Modern Error Handling with C++23
With the introduction of std::expected
in C++23 and the use of std::error_code
form C++11, it is finally possible to use error codes in C++ without the usual hassle.
Using the Modern Error Handling
Let's first see an example of how this could look like. A function that might return an error could be defined like this:
::expected<int, std::error_code>
std
The nodiscard
attribute (introduced with C++17) is not required, but I strongly recommend using it, so that the compiler will issue a warning, if the result of a call to the function is not used.
The function can be called then just as shown here:
// no need for output parameters, the potential error and the
// result can both just be retrieved as a return parameter
const auto result = ;
// check for a valid result to do error handling (expected)
if else
But what if we do not actually can handle the error, but want to forward it to our caller? Just return the error in another std::unexpected
:
const auto result = ;
if return ;
;
Compared to the indeed very nice error handling in Rust, we are missing a bit of syntactical sugar, but the usage pattern is quite close:
let result = divide?;
print!;
Implementing the Modern Error Handling
Let's see then, how the modern error handling can actually be implemented in C++. Using std::expected
is straightforward and could directly be used with your existing error codes if you enabled the C++23 standard on your compiler (see also at cppreference):
// does not need to be an enum class, could also be plain integers
;
::expected<int, math_error>
std
int
Combining it with std::error_code
instead of plain error codes, however, has some advantages like:
- multiple enums can be used to define error codes in the application and the linker ensures with the help of
std::error_category
that every error code is unique - different error code types from different parts of the system can be forwarded by a function, without even needing to know which concrete types are defined
- it can be casted to bool, hence there is no need to compare to some "no error" value
- it contains a human-readable string that can be used for logging, for example
How would the same example look with std::error_code
then? Let's first define our error codes enum and our custom error category:
// The enum will all error codes of this part of the software (for one
// specific error category). As many as needed can be defined for
// different subsystems or libraries.
;
// The concrete error category does not need to be part of any interface.
// Hence, you would usually define it in an anonymous namespace in an
// implementation file.
// namespace
// This function needs to be implemented to allow for automatic
// conversions of the enum to instances of std::error code. So that you
// only need do 'return math_error:division_by_zero', for example, in a
// function that returns a std::error_code.
std::error_code
// The enum must be defined to an error code enum within the std namespace.
// This will lead to the templated conversion functions from an enum value
// to an error code of the std::error_code class to be instantiated for
// this enum class. They will internally call the above defined function
// make_error_code() via argument dependent lookup then.
// namespace std
There is a bit of boiler-plate code needed, for defining an error category and code. However, this needs to be done only once for every category and you usually won't need many categories. Could be only one for the application code, for example, and one for a 3rd-party dependency. After defining, it can then be used like that:
// include the error category and code definition from above
// or a corresponding header file
::expected<int, std::error_code>
std
int
Dynamic Memory?
The major disadvantage of std::error_code
that would prevent me from using it on embedded devices is the use of std::string
in the interface of std::error_category
(and also in the implementations of std::error_code
and std::error_condition
):
virtual std::string ;
Implementing an error category, would lead to the introduction of dynamic memory via std::string
, which is usually unwanted in the firmware development. Luckily, the only things that are needed for implementing system_error
are to provide a std::error_category
class a std::error_code
class and optionally std::error_condition
class. The latter could be left out, if the feature or error conditions are not needed.
All of those classes are quite small and simple, as you could check out in the STL implementation from the LLVM project, for example. Let's see what we need to implement for a system_error
without dynamic memory.
The source code of a full implementation can be found in my Zephyr example repository.
We need a slightly different interface for the error_category
class compared to the version from the STL to avoid dynamic memory. Its interface would look like this then:
;
Similarly for the error_code
class, where we also need to replace the usage of std::string
:
;
As you might have spotted in the interface definitions already, there are two instances of std::error_category
required to be provided by an implementation: a system category and a general category. For compatibility and for being able to default-construct error_code
instances, it makes sense to implement those instances as well:
// implementation of the generic error category
;
// one instance must be created in an implementation file
const generic_error_category ;
// this instance should be returned by a function
const error_category &
// implementation of the system error category
;
// one instance must be created in an implementation file
const system_error_category ;
// this instance should be returned by a function
const error_category &
After doing the necessary implementations for the error_category
, error_code
and optionally the error_condition
, the error handling can be used without dynamic memory.
Instead of doing your own implementation, feel free to check out what I already implemented in my Zephyr example repository.