⬆️ ⬇️

Support for system errors in C ++

Foreword



I have been wondering for a long time whether it is necessary to translate this already known series of articles called System error support in C ++ 0x, which tells about <system_error> and error handling. On the one hand, it was written in 2010 and I simply may be considered necrophilic, but on the other hand, very little information on this topic in RuNet is available and many fairly recent articles refer to this cycle, which suggests that it does not lose its relevance to this day. day.



Therefore, I decided that to perpetuate this work in granite on Habré would be a good idea.



I just want to warn you that I don’t have the experience of a translator and in general May English is out of trouble. And chagrin. So I will be glad to your criticism and suggestions, preferably in a personal.

')

So let's get started.



Part 1



Introduction



Among the new functions of the standard library in C ++ 0x there is a small header file called <system_error> . It provides a set of tools for managing, suddenly, system errors.



The main components defined in it are:





I had a hand in the design of this module, so in the series of my articles I will try to tell about the reasons for its appearance, its history, and the intended use of its components.



Where to get?



Full implementation, as well as support for C ++ 03, is included in Boost . I assume that at the moment this is probably the best proven implementation in terms of portability. Of course, you should write boost::system:: , not std:: .



The implementation is included in GCC 4.4 and later. However, you must compile your program with the -std = c ++ 0x option to use it.



Finally, Microsoft Visual Studio 2010 will come with an implementation of these classes [but with limitations] . The main limitation is that system_category() does not represent a Win32 error as it was intended. More on what this means will be said later.



(Note that these are only implementations of which I know. There may be others).



[Translator's note: of course, this information has long been outdated, now <system_error> is an integral part of the modern standard library]



Short review



The following are the types and classes defined in <system_error> , in a nutshell:





Principles



This section lists some basic principles that I adhered to when designing a module. (I can not speak for the rest of the participants). As with most software projects, some of them were targets from the start, and some arose in the process.



Not all errors are exceptional.



Simply put, exceptions are not always the right way to handle errors. (In some circles this statement is controversial, although I really do not understand why.)



For example, in network programming, errors such as are commonly encountered:





Of course, they can be exceptional circumstances, but equally they can be processed as part of the normal flow of control. If you reasonably expect this to happen, this is not exceptional. Respectively:





Another requirement, in the case of asio, was a way to pass the result of an asynchronous operation to a completion handler. In this case, I want the error code to be an argument to the callback handler.



(An alternative approach is to provide a means for reconstructing exceptions inside a handler, such as the asynchronous .NET BeginXYZ/EndXYZ . In my opinion, this design adds complexity and makes the API more error-prone.)



[Translator's note: now this tool can be std::exception_ptr from C ++ 11]



Last but not least, exceptions cannot be used in some areas due to code size and performance limitations.



In general, you need to be pragmatic, not dogmatic. It is best to use any error mechanism in terms of clarity, correctness, limitations and, yes, even personal taste. Often the correct criterion for deciding between an exception and an error code is the method of use. This means that the presentation of the system error should support both [options] .



Errors come from several sources.



The C ++ 03 standard recognizes errno as the source of error codes. It is used by stdio functions, some mathematical functions, and so on.



On POSIX platforms, many system operations use errno to transmit errors. POSIX defines additional error codes errno to cover these cases.



Windows, on the other hand, does not use errno outside of the standard C library. Windows API calls usually report errors via GetLastError() .



When considering network programming, the getaddrinfo family of functions uses its own set of error codes (EAI _...) on POSIX, but shares the “namespace” GetLastError() in Windows. Programs that integrate other libraries (for SSL, regular expressions, and so on) will encounter other categories of error codes.



Programs must be able to manage these error codes in a consistent manner. I am particularly interested in the [way] that will allow combining operations to create higher level abstractions. Combining system calls, getaddrinfo , SSL and regular expressions into one API should not force the user of this API to deal with the “explosion” of error code types. Adding a new source of errors to the implementation of this API should not change its interface.



The possibility of custom extensions



Standard library users should be able to add their own sources of error. This feature can be used simply to integrate a third-party library, but it is also associated with the desire to create higher-level abstractions. When developing a protocol implementation such as HTTP, I want to be able to add a set of error codes that correspond to the errors defined in the RFC.



Preservation of the original error code



This was not one of my original goals: I thought the standard would provide a set of well-known error codes. If the system operation returned an error, the library should translate the error into well-known code (if such a mapping made sense).



Fortunately, someone pointed out to me the problem of my approach. The translation of the error code clears the information: the error returned by the main system call is lost. This may not be very important from the point of view of the program’s control flow, but it is very important for supporting the program. There is no doubt that programmers will use a standardized error code for logging and tracing, and the initial error may be vital for diagnosing problems.



This final principle is perfectly combined with my theme for the second part: error_code vs error_condition . Keep in touch.



Part 2



error_code vs error_condition



Of the 1000+ pages of the C ++ 0x standard, a casual reader should notice one thing: error_code and error_condition look almost identical! What's happening? These are the consequences of mindless copy-paste?



What matters is what you do about it.



Let's look at the descriptions that I gave in the first part, once again:





Classes are different because they are intended for different purposes. As an example, consider a hypothetical function called create_directory() :



 void create_directory ( const std::string & pathname, std::error_code & ec ); 


Which you call as follows:



 std::error_code ec; create_directory("/some/path", ec); 


The operation can fail for various reasons, for example:





Whatever the cause of the failure, when the create_directory() function returns control ec will contain an error code specific to the OS. On the other hand, if the call was successful, then ec will have a zero value. This is a tribute to tradition (used by errno and GetLastError() ), when zero indicates success, and any other values ​​indicate an error.



If you are only interested in whether the operation was successful or unsuccessful, you can use the fact that error_code easily converted to bool :



 std::error_code ec; create_directory("/some/path", ec); if(!ec) { //Success. } else { //Failure. } 


However, suppose you are interested in checking for the error “directory already exists”. If this error happens, our hypothetical program can continue to work. Let's try to implement this:



 std::error_code ec; create_directory("/some/path", ec); if(ec.value() == EEXIST) //No! ... 


This code is incorrect. It can make money on POSIX platforms, but do not forget that ec will contain a platform-specific error. On Windows, the error is likely to be ERROR_ALREADY_EXISTS . (Worse, the code does not check the category of the error code, but we'll talk about this later.)



The rule of thumb is that if you call error_code::value() , then you are doing something wrong.



So you have a platform- EEXIST error code ( EEXIST or ERROR_ALREADY_EXISTS ) that you want to match with the [platform-independent] error condition (“directory already exists”). Yes, that's right, you need error_condition .



Comparison error_code and error_condition



Here is what happens when comparing the error_code and error_condition objects (that is, when using the == operator or! = Operator):





I hope it is now obvious that you should compare your platform- ec error code ec with the error_condition object, which represents the “directory already exists” error. Just for this case, C ++ 0x provides std::errc::file_exists . This means that you should write:



 std::error_code ec; create_directory("/some/path", ec); if(std::errc::file_exists == ec) ... 


This works because the developer of the standard library has determined the equivalence between the error codes EEXIST or ERROR_ALREADY_EXISTS and the error condition std::errc::file_exists . Later I will show how you can add your own error codes and conditions with corresponding definitions of equivalence.



(Note that, to be precise, std::errc::file_exists is one of the enum values ​​from the enum class errc . For now, you should think of the enumerated values ​​of std::errc::* as labels for error_condition constants. In I will explain the next part how it works.)



How to find out what conditions you can check?



Some of the new library functions in C ++ 0x have an error condition section. These sections list the error_condition constants and the conditions under which equivalent error codes are generated.



A bit of history



The original error_code class was proposed for TR2 as an auxiliary component in the file system libraries and network libraries. In that design, the error_code constant was implemented so that it, whenever possible, corresponded to a platform-specific error. If a match is impossible or there are several matches, the library implementation converts a platform-specific error to standard error_code .



In the email discussions, I learned about the value of maintaining the original error code. Subsequently, the generic_error class was prototyped, but it did not suit me. A satisfactory solution was found when renaming generic_error to error_condition . In my experience, naming is one of the most difficult problems in the field of computer science, and choosing a good name is the main job.



In the next part, we will look at the mechanism that makes the enum class errc work as a set of constants for error_condition .



Part 3



Enumerated values ​​as class constants



As we have seen, the <system_error> header file defines a class enum errc as follows:



 enum class errc { address_family_not_supported, address_in_use, ... value_too_large, wrong_protocol_type, }; 


The enumerated values ​​of which are constants for error_condition :



 std::error_code ec; create_directory("/some/path", ec); if(std::errc::file_exists == ec) ... 


Obviously, an implicit conversion from errc to error_condition is used here using a single argument constructor. Simply. Right?



It's not quite that easy.



There are several reasons why it is a bit more complicated:





So, although it is true that the line:



 if(std::errc::file_exists == ec) 


implicitly converted from errc to error_condition , there are a few more steps.



Step 1: Determine whether the enumerated value is an error code or a condition



For registration of types of transfers two templates are used:



 template<class T> struct is_error_code_enum: public false_type {}; template<class T> struct is_error_condition_enum: public false_type {}; 


If a type is registered using is_error_code_enum<> , then it can be implicitly converted to error_code . Similarly, if a type is registered using is_error_condition_enum<> , it can be implicitly converted to error_condition . By default, types are registered without conversion (hence the use of false_type above), but the enum class errc registered as follows:



 template<> struct is_error_condition_enum<errc>: public true_type {}; 


Implicit conversion is performed using conditionally resolved conversion constructors. This is probably implemented using SFINAE , but for simplicity you need to think of it as:



 class error_condition { ... //Only available if registered //using is_error_condition_enum<>. template<class ErrorConditionEnum> error_condition(ErrorConditionEnum e); ... }; class error_code { ... //Only available if registered //using is_error_code_enum<>. template<class ErrorCodeEnum> error_code(ErrorCodeEnum e); ... }; 


Therefore, when we write:



 if(std::errc::file_exists == ec) 


The compiler chooses between these two overloads:



 bool operator == ( const error_code & a, const error_code & b ); bool operator == ( const error_code & a, const error_condition & b ); 


It will select the latter, since the error_condition conversion constructor is available, but error_code not.



Step 2: match the error value with the error category



The error_condition object contains two attributes: a value and a category. Now, when we got to the constructor, they need to be initialized correctly.



This is achieved thanks to the constructor having the call to the function make_error_condition() .

The ability to custom extension is implemented using the ADL mechanism. Of course, since errc is located in the std , ADL finds make_error_condition() in the same place.



The implementation of make_error_condition() is simple:



 error_condition make_error_condition(errc e) { return error_condition ( static_cast<int>(e), generic_category() ); } 


As you can see, this function uses the error_condition constructor with two arguments to explicitly specify both the error value and the category.



If we were in the error_code conversion constructor (for a properly registered enumeration type), the called function would be make_error_code() . The rest of the construction of error_code and error_condition same.



Explicit conversion to error_code or error_condition



Although error_code is primarily intended for use with platform-specific errors, portable code may want to create error_code from an enumerated errc value. For this reason, the [functions] make_error_code(errc) and make_error_condition(errc) . Portable code can use them as follows:



 void do_foo(std::error_code & ec) { #if defined(_WIN32) //Windows implementation ... #elif defined(linux) //Linux implementation ... #else //do_foo not supported on this platform ec = make_error_code(std::errc::not_supported); #endif } 




A little more history



Initially, the error_code constants in <system_error> were defined as objects:



 extern error_code address_family_not_supported; extern error_code address_in_use; ... extern error_code value_too_large; extern error_code wrong_protocol_type; 


The LWG was concerned about the costs due to the large number of global objects and requested an alternative solution. We investigated the possibility of using constexpr , but in the end it turned out to be incompatible with some other aspects of <system_error> . Thus, only the conversion from the enumeration remained, since it was the best design available.



Next, I'll begin to show how you can add your own error codes and conditions.



Part 4



Creating your own error codes



As I said in the first part, one of the principles of <system_error> is extensibility.This means that you can use the mechanism just described to define your own error codes.



In this section, I will describe what you need to do. As a basis for a working example, suppose you are writing an HTTP library and you need errors that correspond to HTTP status codes .



Step 1: Determine Error Values



First you need to define a set of error values. If you are using C ++ 0x, you can use the class enumsame one std::errc:



 enum class http_error { continue_request = 100, switching_protocols = 101, ok = 200, ... gateway_timeout = 504, version_not_supported = 505 }; 


Errors are assigned values ​​according to HTTP status codes. The importance of this will become apparent when it comes to the use of error codes. Regardless of which values ​​you choose, errors must have non-zero values. As you remember, an object <system_error>uses a convention in which zero means success.



You can use the regular (i.e., C ++ 03 compatible) enumby dropping the keyword class:



 enum http_error { ... }; 


Note: class enum differs from the enumfact that the first one encloses the names of the enumerated values ​​in the class scope [while the second one “throws” them into the global scope) . To access the enumeration values, you must specify the name of the class, for example http_error::ok. You can emulate this behavior by wrapping the usual enumnamespace [ namespace] :



 namespace http_error { enum http_error_t { ... }; } 


In the rest of this example, I will use enum class. Applying a namespace approach remains as an exercise for the reader.



[Translator's note: in fact, they differ not only in scope enum classbut also prohibits implicit conversion of enumerated values ​​to other types]



Step 2: Define the class error_category



An object error_codeconsists of an error value and a category. The error category defines what the given enumeration value specifically means. For example, 100 may refer to both http_error::continue_request, and std::errc::network_down(ENETDOWN in Linux), and maybe something else.



To create a new category, you need to inherit a class from error_category:



 class http_category_impl: public std::error_category { public: virtual const char * name() const; virtual std::string message(int ev) const; }; 


At the moment, this class will implement only pure virtual functions error_category.



Step 3: Give the category a human-readable name.



A virtual function error_category::name()must return a string identifying the category:



 const char * http_category_impl::name() const { return "http"; } 


This name does not have to be completely unique, as it is used only when writing the error code in std::ostream. However, it would be desirable to make it unique within this program.



Step 4: Convert Error Codes To Strings



The function error_category::message()converts the error value to the string describing it:



 std::string http_category_impl::message(int ev) const { switch(ev) { case http_error::continue_request: return "Continue"; case http_error::switching_protocols: return "Switching protocols"; case http_error::ok: return "OK"; ... case http_error::gateway_timeout: return "Gateway time-out"; case http_error::version_not_supported: return "HTTP version not supported"; default: return "Unknown HTTP error"; } } 


When you call a function error_code::message(), error_codein turn, it calls the above virtual function to get an error message.



It is important to remember that these error messages should be autonomous. They can be recorded (in a log file, say) at that point in the program where additional context is not available. If you wrap an existing API that uses error messages with “inserts”, you will have to create your own messages. For example, if the HTTP API uses a message string "HTTP version %d.%d not supported", the equivalent offline message will be "HTTP version not supported".



Module<system_error>does not provide any help when it comes to localizing these messages. It is likely that messages originating from the error categories of the standard library will be based on the current locale. If your program requires localization, I recommend using the same approach.



( A bit of history: The LWG recognized the need for localization, but there was no design that satisfactorily agreed localization with extensibility. Instead of collecting committees to address this issue, the LWG chose not to say anything about the localization of error messages in the standard.)



Step 5: unique category identification



The object identifier inherited from error_categoryis determined by its address. This means that when you write:



 const std::error_category & cat1 = ...; const std::error_category & cat2 = ...; if(cat1 == cat2) ... 


The condition ifis evaluated as if you wrote:



 if(&cat1 == &cat2) ... 


Following the example set by the standard library, you must provide a function to return a reference to the category object:



 const std::error_category & http_category(); 


This function should always return a reference to the same object. One way to do this is to define a global object in the source file and return a link to it:



 http_category_impl http_category_instance; const std::error_category & http_category() { return http_category_instance; } 


However, global variables cause problems with the order of initialization between modules. An alternative approach is to use a local static variable:



 const std::error_category & http_category() { static http_category_impl instance; return instance; } 


In this case, the category object is initialized at first use. C ++ 0x also ensures that initialization is thread safe. (C ++ 03 did not give such a guarantee).



History: In the early design stages, we considered using a whole number or string to identify categories. The main problem with this approach was to ensure uniqueness in combination with extensibility. If a category is identified by a whole number or string, what prevents conflicts between two unrelated libraries? Using the same identity. Furthermore, it makes it possible to make error and polymorphic polymorphic errors.



Step 6: Build error_code from enum



As I showed in part 3, the implementation <system_error>requires a function with a name make_error_code()in order to associate the error value with a category. For HTTP errors, this function might look like this:



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


For completeness, you should also provide an equivalent function for building error_condition:



 std::error_condition make_error_condition(http_error e) { return std::error_condition ( static_cast<int>(e), http_category() ); } 


Since the implementation <system_error>finds these functions using ADL, you must place them in the same namespace as the type http_error.



Step 7: write for implicit conversion to error_code



So that the enumerated values http_errorcan be used as constants error_code, enable the transformation constructor using the template is_error_code_enum:



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




Step 8 (optional): define default error conditions



Some of the errors you describe may have similar error conditions from errc. For example, the HTTP status code 403 Forbidden means the same as std::errc::permission_denied.



The virtual function error_category::default_error_condition()allows you to define an error condition equivalent to a given error code. (The definition of equivalence was described in the second part.) For HTTP errors, you can write:



 class http_category_impl: private std::error_category { public: ... virtual std::error_condition default_error_condition(int ev) const; }; ... std::error_condition http_category_impl::default_error_condition(int ev) const { switch(ev) { case http_error::forbidden: return std::errc::permission_denied; default: return std::error_condition(ev, *this); } } 




If you decide not to redefine this virtual function, it error_conditionwill have the same error and category as error_code. This is the default behavior, as in the example shown above.



Using Error Codes



Now you can use enumerated values http_erroras constants error_code, like when setting the error:



 void server_side_http_handler ( ..., std::error_code & ec ) { ... ec = http_error::ok; } 


and when checking it:



 std::error_code ec; load_resource("http://some/url", ec); if(http_error::ok == ec) ... 


[Translator's note: it should be noted that with such an implementation, the principle described above will not work - zero value = success - respectively, casting boolwill not work either]



Since error values ​​are based on HTTP status codes, we can also set error_codedirectly from the response :



 std::string load_resource ( const std::string & url, std::error_code & ec ) { //send request ... //receive response ... int response_code; parse_response(..., &response_code); ec.assign(response_code, http_category()); ... } 


In addition, you can use this method when wrapping errors created by an existing library.



Finally, if you defined an equivalence relation in step 8, you can write:



 std::error_code ec; data = load_resource("http://some/url", ec); if(std::errc::permission_denied == ec) ... 


without having to know the exact source of the error condition. As explained in part 2, the original error code (for example, http_error::forbidden) is preserved, so no information is lost.



In the next part, I will show how to create and use error_condition.



Part 5



Creating your own error conditions



The extensibility of the module is <system_error>not limited to error codes: error_conditionit can also be expanded.



Why create your own error conditions?



To answer this question, let's return to the differences between error_codeand error_condition:







It offers some use cases for custom error conditions:





As you will see below, the definition is error_conditionsimilar to the definition error_code.



Step 1: Determine Error Values



You need to create enumfor error values, similarly std::errc:



 enum class api_error { low_system_resources = 1, ... name_not_found, ... no_such_entry }; 


The actual values ​​that you use are not important, but you must make sure that they are different and non-zero.



Step 2: Define the class error_category



An object error_conditionconsists of an error value and a category. To create a new category, you need to inherit a class from error_category:



 class api_category_impl: public std::error_category { public: virtual const char * name() const; virtual std::string message(int ev) const; virtual bool equivalent(const std::error_code & code, int condition) const; }; 


Step 3: Give the category a human-readable name.



A virtual function error_category::name()must return a string identifying the category:



 const char * api_category_impl::name() const { return "api"; } 


Step 4: convert error conditions to strings



The function error_category::message()converts the error value to the string describing it:



 std::string api_category_impl::message(int ev) const { switch(ev) { case api_error::low_system_resources: return "Low system resources"; .. } } 


However, depending on your use case, a call error_condition::message()may not be likely. In this case, you can use the abbreviation and just write:



 std::string api_category_impl::message(int ev) const { return "api error"; } 


Step 5: Implement Error Equivalence



A virtual function is error_category::equivalent()used to determine the equivalence of error codes and conditions. There are two overloads of this feature. First:



 virtual bool equivalent(int code, const error_condition & condition) const; 


used to establish equivalence between error_codein the current category and arbitrary error_condition. Second overload:



 virtual bool equivalent(const error_code & code, int condition) const; 


defines equivalence between error_conditionin the current category and arbitrary error_code. Because you create error conditions, you need to override the second overload.



The definition of equivalence is simple: return trueif you want it to error_codebe equivalent to your condition, otherwise return false.



If you intend to abstract from platform-specific errors, you can implement it error_category::equivalent()as follows:



 bool api_category_impl::equivalent(const std::error_code & code, int condition) const { switch(condition) { ... case api_error::name_not_found: #if defined(_WIN32) return code == std::error_code(WSAEAI_NONAME, system_category()); #else return code == std::error_code(EAI_NONAME, getaddrinfo_category()); #endif ... default: return false; } } 


(Obviously, it getaddrinfo_category()also needs to be defined somewhere.)



Checks can be complex, and other constants can also be reused error_condition:



 bool api_category_impl::equivalent(const std::error_code & code, int condition) const { switch(condition) { case api_error::low_system_resources: return code == std::errc::not_enough_memory || code == std::errc::resource_unavailable_try_again || code == std::errc::too_many_files_open || code == std::errc::too_many_files_open_in_system; ... case api_error::no_such_entry: return code == std::errc::no_such_file_or_directory; default: return false; } } 


Step 6: unique category identification



You must define a function to return a reference to the category object:



 const std::error_category & api_category(); 


which always returns a link to the same object. As with error codes, you can use a global variable:



 api_category_impl api_category_instance; const std::error_category & api_category() { return api_category_instance; } 


or you can use static thread-safe variables from C ++ 0x:



 const std::error_category & api_category() { static api_category_impl instance; return instance; } 


Step 7: Build error_condition from enum



An implementation <system_error>requires a function with a name make_error_code()in order to associate an error value with a category:



 std::error_condition make_error_condition(api_error e) { return std::error_condition ( static_cast<int>(e), api_category() ); } 


To complete the picture, you also need to define an equivalent function for the construction error_code. I will leave this as an exercise for the reader.



Step 8: write for implicit conversion to error_condition



Finally, in order for enumerated values ​​to api_errorbe used as constants error_condition, enable the transform constructor using a template is_error_condition_enum:



 namespace std { template<> struct is_error_condition_enum<api_error>: public true_type {}; } 


Use of error conditions



Now enumerated values api_errorcan be used as constants error_condition, as well as those defined in std::errc:



 std::error_code ec; load_resource("http://some/url", ec); if(api_error::low_system_resources == ec) ... 


As I have said several times, the original error code is preserved and information is not lost. It does not matter whether the error code came from the operating system or from the HTTP library with its own category of errors. Your custom error conditions will work equally well anyway.



In the next, probably the last, part I will explain how to create APIs that use <system_error>.



Afterword



Alas, despite the promises of the author, the cycle of articles was not completed. The next part did not come out. And it is unlikely to come out.

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



All Articles