C++23 - std::expected, the superior way of returning a value or an error
In today's post, I like to jump in time and fast forward to what is coming with C++23, a new data type in the STL
std::expected (P0323). The idea behind this data type isn't new. The idea was originally brought up by Andrei Alexandrescu. He explained it deeper in his CppCon talk Expect the expected.
This new data type,
std::expected, contains either the return or the error value. However, the function returns a
std::expected as a data type even in an error case.
You can compare it with C++17's
std::optional, or maybe with a
std::expected contains either a success or an error value. You get several advantages from this new data type.
(Too?) close coupling of error information
Let's start looking at a code example. Say you have a simple wrapper around the POSIX
open. In C++20, such a wrapper may look like this:
1 2 3 4 5 6
The result is that all call sides check for
-1 and use
errno for potential error detection. For our example, you might want to generate different error messages depending on whether the file does not exist or you don't have permission to access the file.
error mean that you drag API elements from POSIX all over into our code, where you end up writing a lot of code like this:
1 2 3 4 5 6 7 8 9 10 11
Do you really want this? What if you want to stay clean in the C++ world? The simple wrapper could do better than just using a
The answer is that you don't really want this mix. Let's have a look at how
OpenFile looks once you sprinkle
1 2 3 4 5 6 7 8 9 10
With the help of
std::expected, you can move the
-1 and the access to
OpenFile. In A, you handle the error case, grab the value of
errno and stash it into
You have a hopefully special case in this example because it is always better to have a dedicated type for the error code. The return and error types are of the same data types. How does one distinguish between the two? The answer is by using another data type,
std::unexpected. You may know
std::unexpected from previous C++ standards. In case you haven't heard, the original
std::unexpected was deprecated in C++17. The data type I'm referring to here is the C++23 incarnation which has nothing to do with the original function.
std::unexpected stored the error value and is assignable to a
std::expected, given that the error data type is the same for both.
The nice additional benefit you get from this helper type is that you can spot the error case, or cases, quickly in our code. You only have to look out for
For the good case, the success path, you simply write the return value as you always do. No additional effort is required, as you can see in B.
With the change to
OpenFile, you also have to refactor the using side of our code. With the powers of
std::expected, the code now looks like this:
1 2 3 4 5 6 7 8 9 10 11
Additionally, we can also eliminate the
errno values by removing the last dependency to POSIX in our using code part. You could, for example, map the value to a class enum. The nice benefit of this is that the compiler will warn us if you do not handle an enumeration in a switch statement.
Delaying an exception.
Returning to Andrei Alexandrescu's talk, what if you call
value on a
std::expected which contains an
unexpected? Well, in this case, an exception is thrown. This approach is a nice way to delay an exception up to a certain point. For example, in our
OpenFile example, there could be cases where not opening the file is a fatal error, regardless of the reason. In such a case, you can spare us checking for the error. You can simply try to access the success value. If no value exists, a
bad_expected_access<E> exception is thrown, where
E is the error type of
Other parts of the interface
The design of
expected comes with a
value_or function. This can be handy if an error gets translated into a default good value.
std::optional, you can access the value with
value, by dereferencing the expected object or via pointer-like access. You can replace the
if(not res.has_value) with
if(!res) like for a