RSS | GitHub |
Many C libraries follow the POSIX tradition of returning errors as integers. Some libraries reuse the pre-defined errno
values, while others define their own library-specific values.
We show how to integrate the error values of such C libraries with std::error_code
. The choice of std::error_code
gives us several advantages: (i) errors are type-erased, (ii) errors are propagated without loss of information, and (iii) errors are interoperable.
Figure 1: Anatomy of std::error_code. |
std::error_code
holds two member variables: an error value of type int
, and a pointer to a library-specific category that represents the error type. The category is a singleton which inherits from std::error_category
.
Each library must define its own error category that overrides the name()
and message()
functions of std::error_category
. The former returns the name of the category, and the latter a textual representation of a concrete error value. The category can be considered a polymorphic std::type_info
that must be implemented manually and therefore works without RTTI.
Two std::error_code
objects are equal if they have the same error value and they point to the same category singleton.
std::error_code
is type-erased, which means that its type remains the same regardless of which error types it embeds. This is achieved because the error category is a polymorphic singleton, and because std::error_code
stores the address of a category singleton, no object-slicing will take place when std::error_code
is copied. Polymorphism is used here as a type-erasure technique.
Type-erasure gives us some advantages:
The running example here is C++ adaptors for audio codec libraries.
These libraries are particularly interesting for our purposes, because although they implement different audio codecs, and they are written by different developers, their error values share a family-resemblance – there is sufficient overlap between their error values so they can be handled in the same manner. For instance, all audio codecs support a limited amount of sample rates, and they return an error if the caller selects an unsupported sample rate.
At the same time, there are also differences so we cannot translate the error values into a pre-defined set of common errors. We therefore want to propagate the actual error value without loss of information, but we only want to compare and react to well-known errors.
std::error_code
was originally designed to allow multiple subsystems to encapsulate their own error values in a common error container. We can therefore use std::error_code
to propagate the error values from all audio codec libraries. We just have to define an error category for each library.
Furthermore, the audio decoders are paired with a transport mechanism that feeds them with data – e.g. a file reader or network streaming – so we must be able to pass through transport errors as well. We are going to assume that errors from the transport layer are already wrapped in std::error_code
. If not, the techniques described below can be used.
Figure 2: Native adapter for mpg123. |
Suppose we want to create a C++ adapter for the mpg123 audio codec library. Below is a selective part of its error values.
The mpg123_plain_strerror()
function converts an error value into a textual representation.
The C++ adapter simply consists of an error category and a make_error_code()
function that is used to convert mpg123 error values into std::error_code
.
Notice that this header does not include the C library header <mpg123.h>
, so we have not polluted the global namespace with symbols from the C library.
Most of the implementation is boiler-plate code, whose main purpose is to delegate the category message()
call to the mpg123_plain_strerror()
function.
We can define adapters for other audio codec libraries in a similar manner.
Whenever the C++ adapter encounters an error from the mpg123 library, we can use mpg123::make_error_code()
to return a std::error_code
to the caller.
The caller then has several options:
error_code::message()
.error_code
in a std::system_error
exception. Better yet, define an mpg123::error
exception that inherits from std::system_error
, so exceptions from the mpg123 adapter can be distinguished from other system_error
exceptions in the try-catch block.error_code
contains the original error value, the caller must first check that the error code belongs to the mpg123::category
and then compare the error_code::value()
against mpg123_errors
enumerators defined in <mpg123.h>
.If the comparison is done in a .cpp
file, then all options above can be done behind a compilation firewall.
This is a manageable solution, albeit not an elegant one. When we add another audio codec, say libopus, then the caller must check for its error values as well.
Although our printing and throwing use cases are really simple, the checking use case places a heavy burden on the user of our C++ adapters, and the burden grows for each audio codec we wrap. We need something better.
Figure 3: Generic adapter with enumerators. |
A more user-friendly way of checking errors is to define enumerators to compare with. We notice that some of the native error values are already covered by std::errc
, such as invalid_argument
, so we will not include them in our own enumeration.
The caller should be able to write the following. Notice the omission of the C library <mpg123.h>
and <opus/opus.h>
headers. This means that we maintain the compilation firewall.
We define a generic enumeration and error category that we want to use across all audio codec libraries.
As the native error values and the codec::errc
enumeration have different types and numerical values, we need a mapping between them. This mapping is done by our category’s equivalent()
member function.
Finally we have to create a specialization of std::is_error_condition_enum
so our enum can be compared directly with a std::error_code
.
The advantage of the above is that we have moved all knowledge of the mpg123 library back into the C++ adapter.
We have used std::error_condition
in the comparisons in the C++ adapter. We cannot use std::error_code
because it will perform an exact match. Instead std::error_condition
was introduced to enable equivalent comparisons.
The descriptions of std::error_condition
are a bit vague. The C++ standard $[$syserr.errcondition.overview$]$ states that
The class error_condition describes an object used to hold values identifying error conditions. $[$ Note: error_condition values are portable abstractions, while error_code values (19.5.3) are implementation specific. — end note $]$
with no elaboration on what an error condition is. This leaves us with an impression that std::error_condition
could be used to propagate platform-independent errors. This is collaborated by the fact that its interface is almost identical to std::error_code
.
That is not the case. As one of the original designers explains
class error_condition - something that you want to test for and, potentially, react to in your code.
Therefore, a more useful guideline is to:
std::error_code
for error propagation.std::error_code
for comparison within a category.std::error_condition
for comparison between categories.