std::expected<T, E> came originally from an experimental monadic and generic programming library outside of Boost written by Boost and WG21 developers around 2013. Before Outcome v1, I deployed the then Expected into a large codebase and I was dismayed with the results, especially on build times. You can read here how those experiences led me to develop Outcome v1.
std::expected<T, E> is a constrained variant type with a strong preference for the successful type
T which it models like a
std::optional<T>. If, however, there is no
T value then it supplies an ‘unexpected’
E value instead.
std::expected<T, E> was standardised in the C++ 23 standard.
Outcome’s Result type can be configured to act just like Expected if you want that, however ultimately Outcome’s Result doesn’t solve the same problem as Expected, plus Outcome models
std::variant<T, E> rather than
std::optional<T> which we think much superior for many use cases, which to summarise:
If you are parsing input which may rarely contain unexpected values, Expected is the right choice here.
If you want an alternative to C++ exception handling i.e. a generalised whole-program error handling framework, Expected is an inferior choice to alternatives.
Outcome recognises Expected-like types and will construct from them, which aids interoperability.
Predictable runtime overhead on the happy path.
Predictable runtime overhead on the sad path.
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
Ehas a throwing move constructor which typically causes storage blowup.
Works well in all configurations of C++, including C++ exceptions and RTTI globally disabled.
Works well on all niche architectures, such as HPC, GPUs, DSPs and microcontrollers.
Ships with every standard library since C++ 23.
Success-orientated syntax makes doing anything with the
Etype is awkward and clunky.
Results in branchy code, which is slow – though predictably so – for embedded controller CPUs.
Failure to examine an Expected generates a compiler diagnostic, but failure to handle both failure and success does not. This can mean failures or successes get accidentally dropped.
Lack of a
tryoperator makes use tedious and verbose.
Variant storage does have an outsize impact on build times in the same way widespread use of
std::varianthas. This is because implementing exception guarantees during copies and moves of non-trivially-copyable types in union storage involves a lot of work for the compiler on every use of copy and move.