diff --git a/doc/src/content/alternatives/_index.md b/doc/src/content/alternatives/_index.md index a90805e7ad..897d9d3a57 100644 --- a/doc/src/content/alternatives/_index.md +++ b/doc/src/content/alternatives/_index.md @@ -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. diff --git a/doc/src/content/alternatives/error_code.md b/doc/src/content/alternatives/error_code.md index b220fcd869..45e070dcab 100644 --- a/doc/src/content/alternatives/error_code.md +++ b/doc/src/content/alternatives/error_code.md @@ -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: @@ -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. @@ -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`. diff --git a/doc/src/content/alternatives/exceptions.md b/doc/src/content/alternatives/exceptions.md index 2af43533f0..911cd987fe 100644 --- a/doc/src/content/alternatives/exceptions.md +++ b/doc/src/content/alternatives/exceptions.md @@ -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). diff --git a/doc/src/content/alternatives/expected.md b/doc/src/content/alternatives/expected.md index 835a87745a..dcc2423c6c 100644 --- a/doc/src/content/alternatives/expected.md +++ b/doc/src/content/alternatives/expected.md @@ -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. diff --git a/doc/src/content/alternatives/leaf.md b/doc/src/content/alternatives/leaf.md index 1f4615cc08..1dae9847eb 100644 --- a/doc/src/content/alternatives/leaf.md +++ b/doc/src/content/alternatives/leaf.md @@ -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. @@ -27,9 +27,7 @@ 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. @@ -37,9 +35,7 @@ If you need an error handling framework which has predictable sad path overhead #### 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. @@ -47,8 +43,10 @@ If you need an error handling framework which has predictable sad path overhead - 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. diff --git a/doc/src/content/alternatives/outcome.md b/doc/src/content/alternatives/outcome.md index 9d25c72b21..23f17df0bf 100644 --- a/doc/src/content/alternatives/outcome.md +++ b/doc/src/content/alternatives/outcome.md @@ -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`.