In the previous post we have seen how you can create your own error-code enumeration for representing different reasons for failure in your component, and how you can store them and convey them type erased via std::error_code . In this post we will see how you can inspect an error situation encoded in std::error_code , and how you can build useful queries about error conditions.

Let’s recall the task. In the previous post we have created a custom error code enum:

enum class FlightsErrc { // no 0 NonexistentLocations = 10, // requested airport doesn't exist DatesInThePast, // booking flight for yesterday InvertedDates, // returning before departure NoFlightsFound = 20, // did not find any combination ProtocolViolation = 30, // e.g., bad XML ConnectionError, // could not connect to server ResourceError, // service run short of resources Timeout, // did not respond in time };

And we have plugged it into the C++ mechanism for handling error codes, so that FlightsErrc can be interpreted as std::error_code :

std::error_code ec = FlightsErrc::InvertedDates;

This represents errors form the sub-system responsible for finding flight connections. We have another system for finding available seats on the previously found flights. It also can fail for a number of reasons:

enum class SeatsErrc { // no 0 InvalidRequest = 1, // e.g., bad XML CouldNotConnect, // could not connect to server InternalError, // service run short of resources NoResponse, // did not respond in time NonexistentClass, // requested class does not exist NoSeatAvailable, // all seats booked };

Suppose we have plugged it into the system in the similar way, by having defined a yet another private error category, so that this enum can also be interpreted as an error code:

std::error_code ec2 = SeatsErrc::NoResponse;

We could of course compare instances of std::error_code against our enum values:

if (ec == FlightsErrc::InvertedDates) { /* do something */ } else if (ec == SeatsErrc::NoResponse) { /* do something else */ }

But this would usually be a wrong thing to do. What I want to do with an std::error_code is either to log its numerical value (without any if statements), or to tell whether it falls into one of the following conditions:

User sent us an illogical request. There is some problem with the system which the user will not understand but which prevents us from returning the requested answer. No airline we are aware of is able to offer a requested trip.

Regarding logging, we have already implemented it in the previous post. Regarding the classification, we can do it by implementing a custom error condition. This is quite similar to implementing a custom error code: we will define an enumeration and then teach the Standard Library how to use it as an std::error_condition :

enum class FailureSource { // no 0 BadUserInput = 1, SystemError = 2, NoSolution = 3, }; namespace std { template <> struct is_error_condition_enum<FailureSource> : true_type {}; } std::error_condition make_error_condition(FailureSource e);

The difference from registering an error code is that we use type trait is_error_condition_enum instead of is_error_code_enum , and that we define a conversion to error_condition rather than error_code . with the above declarations in place, we can make the following compile (but not link yet):

std::error_condition cond = FailureSource::BadUserInput;

So, the library allows you to register your enumeration as either error_condition or error_code . The difference between the two is:

error_code is used for storing and transmitting error codes as they were produced by originating library, unchanged;

is used for storing and transmitting error codes as they were produced by originating library, unchanged; error_condition is used for performing queries on error_code s, for the purpose of grouping or classification or translation.

And what about std::error_category ? Well, you can think of it as a technical detail for implementing custom error_condition s and error_code s.

You can test an error_code against the desired condition by using equality comparisons:

std::error_code ec = FlightsErrc::InvertedDates; assert (ec == FailureSource::BadUserInput); assert (ec != FailureSource::SystemError);

This already compiles. Now we only have to define the semantics, so that comparison results are those indicated by the assertions. You may consider using operator== somewhat of an ab-usage for this purpose, as it does not retain the properties of equivalence relations, but this is how checking for error conditions is defined in the Standard Library. Or it might be my unconventional usage of the facility to have a condition match to more than one error code value.

Defining semantics for conditions

In order to define the matching of error codes against conditions, we will have to define another category derived from std::error_category , again, in a CPP file:

namespace { // anonymous namespace struct FailureSourceCategory: std::error_category { const char* name() const noexcept override; std::string message(int ev) const override; bool equivalent(const std::error_code& code, int condition) const noexcept override; }; }

The first two functions you are already familiar with from the previous post. The first gives a short name for our category, the second gives text names to the enumerated values:

const char* FailureSourceCategory::name() const noexcept { return "failure-source"; } std::string FailureSourceCategory::message(int ev) const { switch (static_cast<FailureSource>(ev)) { case FailureSource::BadUserInput: return "invalid user request"; case FailureSource::SystemError: return "internal error"; case FailureSource::NoSolution: return "no solution found for specified request"; default: return "(unrecognized condition)"; } }

The third function, equivalent describes for each value of enum FailureSource what values of std::error_code it matches to:

bool FailureSourceCategory::equivalent( const std::error_code& ec, int cond) const noexcept { switch (static_cast<FailureSource>(cond)) { case FailureSource::BadUserInput: return /* TBD */; case FailureSource::SystemError: return /* TBD */; case FailureSource::NoSolution: return /* TBD */; default: return false; } }

Here, ec represent the error code we are inspecting, and cond is the numeric value of our condition enum. The philosophy here is that by default the category is not equivalent to an error_code unless you explicitly indicate that it is. This has some practical implications, because it is not only this function that will be called to determine equivalence.

So, let’s try to define semantics for condition FailureSource::BadUserInput . In case of FlightsErrc we can see that the enumeration is not contiguous: it has already been prepared for categorization:

enum class FlightsErrc { // no 0 NonexistentLocations = 10, // requested airport doesn't exist DatesInThePast, // booking flight for yesterday InvertedDates, // returning before departure NoFlightsFound = 20, // did not find any combination ProtocolViolation = 30, // e.g., bad XML ConnectionError, // could not connect to server ResourceError, // service run short of resources Timeout, // did not respond in time };

between 10 and 20: bad user input,

between 20 and 30: no viable solution,

above 30: internal system error.

In case of SeatsErrc there is no such partitioning, so we have to enumerate all the interesting values. There is one: SeatsErrc::NonexistentClass .

Our condition then can be written as:

case FailureSource::BadUserInput: // for SeatsErrc: if (ec == SeatsErrc::NonexistentClass) return true; // for FlightsErrc if (ec.category() == FlightsCat) return ec.value() >= 10 && ec.value() < 20; // for any other error_code enum: return false;

For FlightsErrc values, in order to perform arithmetical expression we check whether the category of error_code is that specified for FlightsErrc ; but as we know from the previous post, this category has been defined as a private detail, and is inaccessible to us. We did not have to make the category a private detail, and in fact other authors suggest using a factory function for returning the category object for your enum; for instance, see here. But there is still a way to access this category object indirectly: create an error_code of which we are sure it stores a FlightsErrc , and access its category:

const std::error_category& FlightsCat = std::error_code{FlightsErrc{}}.category();

operator== on std::error_category actually compares object addresses. Now that we have matched the category, we just need to check if the numeric value of the error is within the desired range.

Defining other cases is similar, and the entire equivalent function looks like this:

bool FailureSourceCategory::equivalent( const std::error_code& ec, int cond) const noexcept { const std::error_category& FlightsCat = std::error_code{FlightsErrc{}}.category(); switch (static_cast<FailureSource>(cond)) { case FailureSource::BadUserInput: if (ec == SeatsErrc::NonexistentClass) return true; if (ec.category() == FlightsCat) return ec.value() >= 10 && ec.value() < 20; return false; case FailureSource::SystemError: if (ec == SeatsErrc::InvalidRequest || ec == SeatsErrc::CouldNotConnect || ec == SeatsErrc::InternalError || ec == SeatsErrc::NoResponse) return true; if (ec.category() == FlightsCat) return ec.value() >= 30 && ec.value() < 40; return false; case FailureSource::NoSolution: if (ec == SeatsErrc::NoSeatAvailable) return true; if (ec.category() == FlightsCat) return ec.value() >= 20 && ec.value() < 30; return false; default: return false; } }

Now, you might ask, why do we need to define an enumeration, and then these customizations, this error category, if we could have just written three functions: is_system_error() , is_bad_input() , is_no_solution() . We could and this would work for enums FlightsErrc and SeatsErrc , but no other. Suppose all this is a library and someone else will be using it, they will create their own error code and will want it to be recognized by my condition. The C++ framework for error codes allows that: you can teach a new error condition to recognize existing error codes, but also you can teach a new error code to be recognized by existing error conditions. You can do it through a yet another virtual member function in std::error_category :

bool equivalent(int code, const error_condition& cond) const noexcept;

It has the same name equivalent but different set of parameters. So, the way operator== between error codes and error conditions is implemented, it takes both overloads into consideration:

bool operator==(const error_code& lhs, const error_condition& rhs) noexcept { return lhs.category().equivalent(lhs.value(), rhs) || rhs.category().equivalent(lhs, rhs.value()); }

And that’s it. Now we can test our condition:

int main() { std::error_code ec = FlightsErrc::NoFlightsFound; assert (ec == FailureSource::NoSolution); assert (ec != FailureSource::BadUserInput); ec = SeatsErrc::NonexistentClass; assert (ec != FailureSource::NoSolution); assert (ec == FailureSource::BadUserInput); }

For a fully working example see here.