Thursday, April 08, 2010

System error support in C++0x - part 2

[ part 1 ]

error_code vs error_condition

Of the 1000+ pages of C++0x draft, the casual reader is bound to notice one thing: error_code and error_condition look virtually identical! What's going on? Is it a copy/paste error?

It's what you do with it that counts

Let's review the descriptions I gave in part 1:

  • class error_code - represents a specific error value returned by an operation (such as a system call).
  • class error_condition - something that you want to test for and, potentially, react to in your code.

The classes are distinct types because they're intended for different uses. As an example, consider a hypothetical function called create_directory():

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

which you call like this:

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

The operation can fail for a variety of reasons, such as:

  • Insufficient permission.
  • The directory already exists.
  • The path is too long.
  • The parent path doesn't exist.

Whatever the reason for failure, after create_directory() returns, the error_code object ec will contain the OS-specific error code. On the other hand, if the call was successful then ec contains a zero value. This follows the tradition (used by errno and GetLastError()) of having 0 indicate success and non-zero indicate failure.

If you're only interested in whether the operation succeeded or failed, you can use the fact that error_code is convertible-to-bool:

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

However, let's say you're interested in checking for the "directory already exists" error. If that's the error then our hypothetical caller can continue running. Let's have a crack at it:

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

This code is wrong. You might get away with it on POSIX platforms, but don't forget that ec will contain the OS-specific error. On Windows, the error is likely to be ERROR_ALREADY_EXISTS. (Worse, the code doesn't check the error code's category, but we'll cover that later.)

Rule of thumb: If you're calling error_code::value() then you're doing it wrong.

So here you have an OS-specific error code (EEXIST or ERROR_ALREADY_EXISTS) that you want to check against an error condition ("directory already exists"). Yep, that's right, you need an error_condition.

Comparing error_codes and error_conditions

Here is what happens when you compare error_code and error_condition objects (i.e. when you use operator== or operator!=):

  • error_code against error_code - checks for exact match.
  • error_condition against error_condition - checks for exact match.
  • error_code against error_condition - checks for equivalence.

I hope that it's now obvious that you should be comparing your OS-specific error code ec against an error_condition object that represents "directory already exists". C++0x provides one for exactly that: std::errc::file_exists. This means you should write:

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

This works because the library implementor has defined the equivalence between the error codes EEXIST or ERROR_ALREADY_EXISTS and the error condition std::errc::file_exists. In a future instalment I'll show how you can add your own error codes and conditions with the appropriate equivalence definitions.

(Note that, to be precise, std::errc::file_exists is an enumerator of enum class errc. For now you should think of the std::errc::* enumerators as placeholders for error_condition constants. In a later part I'll explain how that works.)

How to know what conditions you can test for

Some of the new library functions in C++0x have "Error conditions" clauses. These clauses list the error_condition constants and the conditions under which equivalent error codes will be generated.

A bit of history

The original error_code class was proposed for TR2 as a helper component for the filesystem and networking libraries. In that design, an error_code constant is implemented so that it matches the OS-specific error, where possible. When a match is not possible, or where there are multiple matches, the library implementation translates from the OS-specific error to the standard error_code, after performing the underlying operation.

In email-based design discussions I learnt the value of preserving the original error code. Subsequently, a generic_error class was prototyped but did not satisfy. A satisfactory solution was found in renaming generic_error to error_condition. In my experience, naming is one of the Hardest Problems in Computer Science, and a good name will get you most of the way there.

Next up, a look at the mechanism that makes enum class errc work as error_condition placeholders.


liancheng said...

Greate post!

I've once use Asio to wrap MySQL C API and porting MySQL error codes to Boost.System. The tricky part is that, to get an error message, you must pass both the error code *and* the MySQL handle, while you can only specify a simple [error code -> error message] map without any other context information (like the MySQL handle). At last, I had to deprecate the message() method of the corresponding error_category subclass.

I wonder how can this issue be solved?

h4tr3d said...

Very very very (very) useful explanation! Thank you Chris!

I am a little confused, why error_category uses `const char*` for `name()` and `std::string` for `message()`... It is bad for embedded.

Anonymous said...

I simply found your web site some days ago and i are reading through it frequently. you have got a wide range of helpful data on the site and that i conjointly really like the particular type of the site at constant room software