Skip to content

Commit

Permalink
Incorporate feedback from Emil into alternatives guide.
Browse files Browse the repository at this point in the history
  • Loading branch information
ned14 committed Jan 10, 2022
1 parent 63cd43b commit 58fb158
Show file tree
Hide file tree
Showing 6 changed files with 18 additions and 18 deletions.
2 changes: 2 additions & 0 deletions doc/src/content/alternatives/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ Outcome [started life in 2014]({{% relref "/history" %}}), entered Boost as Boos
These are listed in order of approximate availability to the C++ ecosystem i.e. in order of appearance.

{{% children description="true" depth="2" %}}

My thanks to Emil Dotchevski for reviewing this summary and providing notes.
6 changes: 3 additions & 3 deletions doc/src/content/alternatives/error_code.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description = "Advantages and disadvantages of `std::error_code`"
weight = 20
+++

`std::error_code` came originally from `boost::error_code` which was designed around 2008 as part of implementing Filesystem and Networking. They are a simple trivially copyable type offering improved type safety and functionality over C enumerations. [You can read more about how `std::error_code` works here]({{% relref "/motivation/std_error_code" %}}). They were standardised in the C++ 11 standard.
`std::error_code` came originally from `boost::error_code` which was designed around 2008 as part of implementing Filesystem and Networking. They are a simple trivially copyable type offering improved type safety and functionality over C enumerations. [You can read more about how `std::error_code` works here]({{% relref "/motivation/std_error_code" %}}). They were standardised in the C++ 11 standard, and have been available in Boost since 2008.

#### Pros:

Expand All @@ -14,7 +14,7 @@ weight = 20

- Unbiased syntax equal for both success and failure requiring explicit code written to handle both.

- Very little bloat added to binaries.
- Very little codegen bloat added to binaries (though there is a fixed absolute overhead for support libraries).

- Once constructed, passing around `std::error_code` instances optimises well, often being passed in CPU registers.

Expand All @@ -30,7 +30,7 @@ weight = 20

- Results in branchy code, which is slow -- though predictably so -- for embedded controller CPUs.

- Because the `std::error_category` instance used in construction comes from a magic static, the compiler inserts an atomic operation around every `std::error_code` construction. This can impact optimisation on compilers with poor optimisation of atomics.
- Because the `std::error_category` instance used in construction comes from a magic static, the compiler inserts an atomic operation around every `std::error_code` construction (e.g. https://godbolt.org/z/oGaf4qe8a). This can impact optimisation on compilers with poor optimisation of atomics.

- The payload of type `int` is incredibly constraining sometimes, especially on 64-bit platforms. It would have been much better if it were `intptr_t` instead of `int`.

Expand Down
4 changes: 2 additions & 2 deletions doc/src/content/alternatives/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ C++ exception throws came in the original C++ 98 standard -- at that time, not a

- Requires RTTI to be enabled or non-standard behaviour results (which is further binary bloat).

- Not available in several major parts of the C++ ecosystem (embedded, games, audio, to a lesser extent financial).
- Not available by tradition or convention in several major parts of the C++ ecosystem (embedded, games, audio, to a lesser extent financial).

- Not available in many niche architectures such as HPC, GPUs, DSPs and microcontrollers.

- Most codebases do not invest in adequate correctness testing of the silent proliferation of failure control flow paths which result in C++ exception throwing code.
- Most codebases do not invest in adequate correctness testing of the silent proliferation of failure control flow paths which result in C++ exception throwing code (exception throws silently generate multitudes of slight variations of sad path control flows).
2 changes: 1 addition & 1 deletion doc/src/content/alternatives/expected.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Outcome recognises Expected-like types and will construct from them, which aids

- Predictable runtime overhead on the sad path.

- Very little bloat added to binaries.
- Very little codegen bloat added to binaries (though there is a fixed absolute overhead for support libraries).

- Variant storage means storage overhead is minimal, except when either `T` or `E` has a throwing move constructor which typically causes storage blowup.

Expand Down
20 changes: 9 additions & 11 deletions doc/src/content/alternatives/leaf.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ As much as Outcome originated in a negative reaction to the then originally prop

- Outcome's Result type encodes the type of the error in the function signature, which could be considered as more brittle and problematic for large scale code refactoring[^1].

- Outcome is intended to be the ultimate error handling framework in a program (i.e. all third party custom error handling flows into Outcome via customisation point adapters), whereas LEAF is intended to be used ad hoc as needed.
- Outcome is more strongly opinionated about being the ultimate error handling framework in a program (i.e. all third party custom error handling is assumed to flow into Outcome via customisation point adapters), whereas LEAF is less strongly opinionated, and yet provides equivalent functionality.

LEAF therefore looks a lot more like standard C++ exception handling, but without the non-deterministic sad path at the cost of a slight impact on happy path runtime performance. LEAF's current design was completed in 2020.
LEAF therefore looks more like standard C++ exception handling, but without the non-deterministic sad path at the cost of a slight impact on happy path runtime performance. LEAF's current design was completed in 2020.

If you need an error handling framework which has predictable sad path overhead unlike C++ exceptions, but you otherwise want similar syntax and use experience to C++ exceptions, LEAF is a very solid choice.

Expand All @@ -27,28 +27,26 @@ If you need an error handling framework which has predictable sad path overhead

- Does not cause branchy code to the same extent as Outcome, and the sad path is deterministic unlike with C++ exceptions.

- Very little bloat added to binaries.

- Sad path control flow is implied in code, same as with C++ exceptions, so code only shows the happy path. This reduces visual code clutter, and eliminates any need for a `try` operator.
- Very little codegen bloat added to binaries (though there is a fixed absolute overhead for support libraries, most of which can be compiled out using a macro if desired).

- Unlike with any of the preceding options, failures nor successes cannot get unintentionally dropped. This is the same strength of guarantee as with C++ exceptions.

- Works well in most configurations of C++, including C++ exceptions and RTTI globally disabled. Does not dynamically allocate memory.

#### Cons:

- Sad path control flow is implied which suits code which almost never fails, but is less suited for code where sad path control flow is not uncommon and/or where auditing during code review of sad path logic is particularly important.

- Requires out of band storage for state e.g. thread local storage, or a global synchronised ring buffer.
- Requires out of band storage for state e.g. thread local storage, or a global synchronised ring buffer[^2].

- If thread local storage is chosen as the out of band storage, transporting LEAF state across threads requires manual intervention.

- If a global ring buffer is chosen as the out of band storage, thread synchronisation with global state is required and the ring buffer can wrap which drops state.

- Thread local storage can be problematic or even a showstopper in many niche architectures such as HPC, GPUs, DSPs and microcontrollers. Global synchronised state can introduce an unacceptable performance impact on those architectures.

- Current compilers at the time of writing do not react well to use of thread local storage, it would seem that elision of code generation is inhibited if thread local state gets touched due to pessimistic assumptions about escape analysis. Given that this impacts all of C and C++ due to the same problem with `errno`, it is hoped that future compilers will improve this. Until then, any code which touches thread local storage or magic statics[^2] will not optimise as well as code which does neither.
- Current compilers at the time of writing do not react well to use of thread local storage, it would seem that elision of code generation is inhibited if thread local state gets touched due to pessimistic assumptions about escape analysis. Given that this impacts all of C and C++ due to the same problem with `errno`, it is hoped that future compilers will improve this. Until then, any code which touches thread local storage or magic statics[^3] will not optimise as well as code which does neither.

[^1]: In Outcome, it is strongly recommended that one chooses a single universal error type for all public APIs such as `std::error_code` or `error` from Experimental.Outcome, so if the programmer is disciplined then the function signature does not expose internal error types. Such single universal error types type erase the original error object, but still allow the original error object to be inspected. This avoids 'exception specifications' which are widely known to not scale well.

[^1]: In Outcome, it is strongly recommended that one chooses a single universal error type for all public APIs such as `std::error_code` or `error` from Experimental.Outcome, so if the programmer is disciplined then the function signature does not expose internal error types. Such single universal error types type erase the original error object, but still allow the original error object to be inspected. This avoids 'exception specifications' which are well known to not scale well.
[^2]: A global synchronised ring buffer implementation does not ship with LEAF, however LEAF exposes customisation points for a bespoke thread local storage implementation which makes implementing one very straightforward.

[^2]: `std::error_code` construction touches a magic static, and therefore Outcome when combined with `std::error_code` also sees a codegen pessimisation. Experimental Outcome's `error` fixes this historical oversight.
[^3]: `std::error_code` construction touches a magic static or calls an extern function, and therefore Outcome when combined with `std::error_code` also sees a codegen pessimisation. Experimental Outcome's `error` fixes this historical oversight.
2 changes: 1 addition & 1 deletion doc/src/content/alternatives/outcome.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Outcome recognises Expected-like types and will construct from them, which aids

- Predictable runtime overhead on the sad path.

- Very little bloat added to binaries.
- Very little codegen bloat added to binaries (though there is a fixed absolute overhead for support libraries if you use Outcome's bundled error types).

- Neither success nor failure is prioritised during use -- types will implicitly construct from both `T` and `E` if it is unambiguous, so no clunky added markup needed to return an `E`.

Expand Down

0 comments on commit 58fb158

Please sign in to comment.