📜 ⬆️ ⬇️

Your own std :: code_error

A couple of words from the translator


Continuing to cover the topic of std::system_error in runet, I decided to translate several articles from the blog Andrzej Krzemieński, which I was advised in the comments to the previous post.

Since these articles have sufficient volume, I decided not to merge them into a heap, as last time, but to publish in the original format.

I also want to warn you that Andrzej Krzemieński has a rather confused style of presentation, which I did not rule. Nevertheless, I act as a translator, not an editor. So, perhaps, to understand some of the theses will have to re-read twice.

Introduction


Recently, in my application, I implemented the “error condition classification” offered by the functional std::error_code . In this post I want to share my experience.
')
C ++ 11 has a rather complicated error condition classification mechanism. You may have come across such concepts as “error code”, “error condition”, “error category”, but it is difficult to find out how good they are and how to use them is not easy. The only valuable source of information on this issue on the Internet is a series of articles from Christopher Kohlhoff, the author of the Boost.Asio library:


[Translator's note: I previously translated this cycle for Habr]

And it was a really good start for me. But I still think that it would be useful to have several sources of information and several explanations of this topic. So, let's begin…

Problem


First, why do I need it. I have a service to search for flights. You tell me from where and where you want to fly, and I offer you specific flights and prices. To do this, my service refers to other services:


Each of these services may return an error for a number of reasons. The reasons for refusal — different for each service — can be listed. For example, the authors of these two services selected the following listings:

 enum class FlightsErrc { //  0 NonexistentLocations = 10, //     DatesInThePast, //      InvertedDates, //    NoFlightsFound = 20, //      ProtocolViolation = 30, // ,  XML ConnectionError, //      ResourceError, //     Timeout, //     }; enum class SeatsErrc { //  0 InvalidRequest = 1, // ,  XML CouldNotConnect, //      InternalError, //     NoResponse, //     NonexistentClass, //     NoSeatAvailable, //    }; 

Here you can notice something. First, the causes of failure are very similar in any service, but they are assigned different names and different numerical values. This is due to the fact that the two services were developed independently by two different teams. It also means that the same numerical value can be related to two completely different conditions, depending on which service reported it.

Secondly, as is evident from the names, the reasons for refusals have different sources:


Why do I really need these different error codes? If any of these errors occur, we want to stop processing the current request from the user. When I cannot offer him a flight, I want to highlight only the following situations:

  1. You made an illogical query.
  2. There are no known flights for your trip.
  3. There is some problem with the system that you do not understand, but which prevents us from giving an answer.

On the other hand, for the purposes of internal audit or search for bugs, we need more detailed information that will be placed in the logs, for example, which system reported a failure and what actually happened. It can be encoded in integer. Any other details, such as the ports to which we tried to connect, or the database we tried to use, will most likely be recorded separately, so the data encoded in the int should be enough.

std :: error_code


The standard library std::error_code designed to store exactly this type of information: a number representing the status, and a “domain”, within which this number is assigned a value. In other words, std::error_code is a pair: {int, domain} . This is reflected in its interface:

 void inspect(std::error_code ec) { ec.value(); //  ec.category(); //  } 

But you almost never need to check std::error_code in this way. As we already said, two things we want to do is to log the state of std::error_code how it was created (without subsequent conversion by higher levels of the application) and use it to answer a specific question, for example: “This error was caused by the user, provided incorrect data? ".

If you are asking yourself why use std::error_code instead of exceptions, let me clarify: these two things are not mutually exclusive. I want to report bugs in my program through exceptions. Inside the exception, instead of strings, I want to store std::error_code , which I can easily check.
std::error_code has nothing to do with getting rid of exceptions. In addition, in my use case, I do not see a good reason to have many different types of exceptions. I will catch them all in one (or two) places, and I’ll learn about different situations by checking the object std::error_code .

Connect your listing


Now we want to adapt std::error_code so that it can store errors from the flight service described above:

 enum class FlightsErrc { //  0 NonexistentLocations = 10, //     DatesInThePast, //      InvertedDates, //    NoFlightsFound = 20, //      ProtocolViolation = 30, // ,  XML ConnectionError, //      ResourceError, //     Timeout, //     }; 

We should be able to convert [values] from our enum to std::error_code :

 std::error_code ec = FlightsErrc::NonexistentLocations; 

But our enumeration must meet one condition: the numeric value 0 should not be an erroneous situation. 0 represents success in any domain (category) of errors. This fact will be used later when checking the object std::error_code :

 void inspect(std::error_code ec) { if(ec) // : 0 != ec.value() handle_failure(ec); else handle_success(); } 

In this sense, the mentioned article incorrectly uses the numerical value 200, indicating success.

So this is what we did: we did not start the FlightErrc from 0. This, in turn, means that we can create an enumeration that does not correspond to any of the listed values:

 FlightsErrc fe {}; 

This is an important characteristic of enums in C ++ (even enumeration classes from C ++ 11): you can create values ​​outside the enumeration range. It is for this reason that the compilers issue a warning in the switch-statement that “not all control paths return a value,” even if you have a case label for each enumerated value.

Now back to the conversion, std::error_code has a conversion constructor pattern that looks more or less like this:

 template<class Errc> requires is_error_code<Errc>::value error_code(Errc e) noexcept: error_code{make_error_code(e)} {} 

(Of course, I used a still non-existent conceptual syntax, but the idea should be clear: this constructor is available only when std::is_error_code<Errc>::value is evaluated as true .)

This constructor is intended for setting up user-connected enumerations to the system. To connect FlightErrc , we need to make sure that:

  1. std::is_error_code<Errc>::value returns true .
  2. The function make_error_code accepting the FlightsErrc type FlightsErrc defined and accessible via the ADL .

As for the first item, we should specialize the standard template:

 namespace std { template<> struct is_error_code_enum<FlightsErrc>: true_type {}; } 

This is one of those situations where declaring something in the std is "legal."

As for the second item, we just need to declare the overload of the function make_error_code in the same namespace as FlightsErrc :

 enum class FlightsErrc; std::error_code make_error_code(FlightsErrc); 

And this is all that is needed to be seen by other parts of the program / library and that we must provide in the header file. The rest is the implementation of the function make_error_code , and we can put it in a separate translation unit (.cpp file).

From this point we can assume that FlightsErrc is an error_code :

 std::error_code ec = FlightsErrc::NoFlightsFound; assert(ec == FlightsErrc::NoFlightsFound); assert(ec != FlightsErrc::InvertedDates); 

Error category declaration


Up to this point, I have said that error_code is a pair: {number, domain} , where the first element uniquely identifies the specific error situation in the domain, and the second uniquely identifies the error domain among all possible error domains that will ever be conceived. But, given that this domain identifier should be stored in one machine word, how can we guarantee that it will be unique to all libraries currently on the market and those that are still ahead? We hide the domain ID as an implementation detail. If we want to use another third-party library with our own enumeration of errors, how can we guarantee that their domain ID will not be equal to ours?

The solution chosen for std::error_code is based on the observation that for each global object (or more formally: a namespace object) a unique address is assigned. Regardless of how many libraries and how many global objects are combined together, each global object has a unique address - this is quite obvious.

To take advantage of this, we need to associate with each type that we want to connect to the system error_code , a unique global object, and then use its address as an identifier. Now the addresses will be used to represent the domain, this is exactly what std::error_code does. But now, when we store some * , the question arises: what should be ? A rather rational choice: let's use the type that can offer us additional benefits. So, the type T is std::error_category , and an additional advantage is in its interface:

 class error_category { public: virtual const char * name() const noexcept = 0; virtual string message(int ev) const = 0; //    ... }; 

I used the name "domain", the standard library uses the name "error category" for the same purpose.

It has pure virtual methods that already offer something: we will store pointers to classes inherited std::error_category : for each new enumeration of errors, we need to get a new class inherited from std::error_category . Typically, having pure virtual methods means selecting objects on the heap, but we will not do such things. We will create global objects and point to them.

There are other virtual methods in std::error_category , which in other cases need to be configured, but we will not have to do this to connect FlightErrc .

Now, for each custom error “domain” represented by a class derived std::error_category , we need to override two methods. The name method returns the short mnemonic name of the category (domain) of errors. The message method assigns a text description for each numeric error value in this domain. To better illustrate this, let's define the error category for our FlightsErrc listing. Remember that this class should only be visible in one translation unit. In other files we will simply use the address of its instance.

 namespace { struct FlightsErrCategory: std::error_category { const char * name() const noexcept override; std::string message(int ev) const override; }; const char * FlightsErrCategory::name() const noexcept { return "flights"; } std::string FlightsErrCategory::message(int ev) const { switch(static_cast<FlightsErrc>(ev)) { case FlightsErrc::NonexistentLocations: return "nonexistent airport name in request"; case FlightsErrc::DatesInThePast: return "request for a date from the past"; case FlightsErrc::InvertedDates: return "requested flight return date before departure date"; case FlightsErrc::NoFlightsFound: return "no filight combination found"; case FlightsErrc::ProtocolViolation: return "received malformed request"; case FlightsErrc::ConnectionError: return "could not connect to server"; case FlightsErrc::ResourceError: return "insufficient resources"; case FlightsErrc::Timeout: return "processing timed out"; default: return "(unrecognized error)"; } } const FlightsErrCategory theFlightsErrCategory {}; } 

The name method provides a short text that is used when streaming std::error_code to things like, for example, logs: this can help you determine the cause of the error. The text does not have to be unique in all enumeration errors: in the worst case, journal entries will be ambiguous.

The message method provides a description text for any numeric value that represents an error in our category. This can be useful when debugging or viewing logs; but you probably don’t want to show this text to users without further processing.

Usually this method is called indirectly. Callers may not know that the numeric value is FlightErrc , so we must explicitly bring it back to FlightErrc . I believe that the example in the above article does not compile because of the static_cast skipped. After casting, there is a risk that we will check a value that does not apply to the enumeration: therefore, we need a default label.

Finally, notice that we initialized the global object of our type FlightErrCategory . This will be the only object of this type in the program. We will need his address, but we will also use his polymorphic properties.

Although the class std::error_category not a literal type, it has a constexpr default constructor. The implicitly declared default constructor of our FlightErrCategory class inherits this property. Thus, we ensure that the initialization of our global object is performed during the initialization of constants, as described in this article, and therefore does not contain any problems with the order of static initialization .

Now the last missing part is the implementation of make_error_code :

 std::error_code make_error_code(FlightsErrc e) { return {static_cast<int>(e), theFlightsErrCategory}; } 

And we are done. Our FlightsErrc can be used as if it were std::error_code :

 int main() { std::error_code ec = FlightsErrc::NoFlightsFound; std::cout << ec << std::endl; } 

The output of this program will be as follows:

flights: 20

A full working example illustrating the above can be found here .

And that's all for today. We have not yet considered how to make useful queries on std::error_code objects, but this will be a topic for another article.

Thanks


I am grateful to Tomasz Kamiński for explaining to me the idea behind std::error_code . In addition to the series of articles from Christopher Kohlhoff, I was also able to learn about the std::error_code from the documentation for the Outcome library from Niall Douglas, here .

Source: https://habr.com/ru/post/340604/


All Articles