Your own error condition

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:

  1. User sent us an illogical request.
  2. There is some problem with the system which the user will not understand but which prevents us from returning the requested answer.
  3. 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;
  • error_condition is used for performing queries on error_codes, 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_conditions and error_codes.

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.

Advertisements
This entry was posted in programming and tagged , . Bookmark the permalink.

5 Responses to Your own error condition

  1. Vinnie Falco says:

    Wow, is this real? I have been working with error codes for a few years now and I had no idea. In fact I have on my TODO for Beast to make different categories for HTTP parsing errors and HTTP stream errors. So you’re telling me I can do this using categories instead? Brilliant

    • Hi Vinnie, and congratulations on getting Beast into Boost. My team has decided to move to Beast, after the verdict.

      It was the primary motivation for starting this blog: that there is so much good stuff in C++ that people do not know about, because it is not advertised, and you can only sometimes learn it by chance.

  2. Artem Tokmakov says:

    This looks intersting! Any thoughts on dealing with error categories when they are used in dlls? Static objects and comparing addresses won’t work well in that scenario. See something like this: https://connect.microsoft.com/VisualStudio/feedback/details/1053790

    • Thanks for bringing this up. Maybe it deserves a separate post. This problem typically do not appear in Linux (unless you explicitly ask for it), it is specific to Windows DLLs, and boils down to the fact that globals may get duplicated in each dll. One typically resolves this problem by making sure the global is defined in one DLL (and no static library), whereas other DLLs and the main executable only reference it. The same problem is faced by Boost.Log library (they require one global place for loggers), and the way they solve it is the guidance in the documentation: “One thing should be noted, though. If your application consists of more than one module (e.g. an exe and one or several dll’s) that use Boost.Log, the library must be built as a shared object. If you have a single executable or a single module that works with Boost.Log, you may build the library as a static library.”

      (see http://www.boost.org/doc/libs/1_64_0/libs/log/doc/html/log/installation/config.html)

  3. Artem Tokmakov says:

    I would really love Log library to not depend on global objects like that 🙂 Same goes for the error_category. Making sure everyone compiles their categories into dll, and making sure you only have at most one dll per category is, let’s put it this way, tricky )

    Although, of course, likely there’s nothing else we can do about the issue at this point.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s