Monday, April 12, 2010

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

[ part 1, part 2, part 3 ]

Creating your own error codes

As I stated in part 1, one of the principles behind the <system_error> facility is user-extensibility. This means that you can use the mechanism just described to define your own error codes.

In this section, I'll outline what you need to do. As a basis for a worked example, I will assume you're writing an HTTP library and need errors that correspond to the HTTP response codes.

Step 1: define the error values

You first need to define the set of error values. If you're using C++0x, you can use an enum class, similar to std::errc:

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

The errors are assigned values according to the HTTP response codes. The importance of this will become obvious when it comes to using the error codes. Whatever values you choose, errors should have non-zero values. As you may recall, the <system_error> facility uses a convention where zero means success.

You can use regular (that is, C++03-compatible) enums by dropping the class keyword:

enum http_error
{
...
};

Note: C++0x's enum class differs from enum in that the former encloses enumerator names in the class scope. To access an enumerator you must prefix the class name, as in http_error::ok. You can approximate this behaviour by wrapping the plain enum in a namespace:

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

For the remainder of this example I will assume the use of enum class. Applying the namespace-wrapping approach is left as an exercise for the reader.

Step 2: define an error_category class

An error_code object consists of both an error value and a category. The error category determines whether a value of 100 means http_error::continue_request, std::errc::network_down (ENETDOWN on Linux), or something else entirely.

To create a new category, you must derive 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;
};

For the moment, this class will implement only error_category's pure virtual functions.

Step 3: give the category a human-readable name

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

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

This name does not need to be universally unique, as it is really only used when writing an error code to a std::ostream. However, it would certainly be desirable to make it unique within a given program.

Step 4: convert error codes to strings

The error_category::message() function converts an error value into a string that describes the error:

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 the error_code::message() member function, the error_code in turn calls the above virtual function to obtain the error message.

It's important to remember that these error messages must stand alone; they may be written (to a log file, say) at a point in the program when no additional context is available. If you are wrapping an existing API that uses error messages with "inserts", you'll have to create your own messages. For example, if an HTTP API uses the message string "HTTP version %d.%d not supported", the equivalent stand-alone message would be "HTTP version not supported".

The <system_error> facility provides no assistance when it comes to localisation of these messages. It is likely that the messages emitted by your standard library's error categories will be based on the current locale. If localisation is a requirement in your program, I recommend using the same approach. (Some history: The LWG was aware of the need for localisation, but there was no design before the group that satisfactorily reconciled localisation with user-extensibility. Rather than engage in some design-by-committee, the LWG opted to say nothing in the standard about localisation of the error messages.)

Step 5: uniquely identify the category

The identity of an error_category-derived object is determined by its address. This means that when you write:

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

the if condition is evaluated as if you had written:

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

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

const std::error_category& http_category();


This function must always return a reference to the same object. One way to do that is to define a global object in a source file and return a reference to that:

http_category_impl http_category_instance;

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

However, using a global does introduce issues to do with order of initialisation across modules. An alternative approach is to use a locally scoped static variable:

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

In this case, the category object is initialised on first use. C++0x also guarantees that the initialisation is thread-safe. (C++03 makes no such guarantee.)

History: In the early design stages, we considered using an integer or string to identify an error_code's category. The main issue with that approach was ensuring uniqueness in conjunction with user extensibility. If a category was identified by integer or string, what was to stop collisions between two unrelated libraries? Using object identity leverages the linker in preventing different categories from having the same identity. Furthermore, storing a pointer to a base class allows us to make error_codes polymorphic while keeping them as copyable value types.

Step 6: construct an error_code from the enum

As I showed in part 3, the <system_error> implementation requires a function called make_error_code() to associate an error value with a category. For the HTTP errors, you would write this function as follows:

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 the equivalent function for construction of an error_condition:

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

Since the <system_error> implementation finds these functions using argument-dependent lookup, you should put them in the same namespace as the http_error type.

Step 7: register for implicit conversion to error_code

For the http_error enumerators to be usable as error_code constants, enable the conversion constructor using the is_error_code_enum type trait:

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

Step 8 (optional): assign default error conditions

Some of the errors you define may have a similar meaning to the standard's errc error conditions. For example, the HTTP response code 403 Forbidden means basically the same thing as std::errc::permission_denied.

The error_category::default_error_condition() virtual function lets you define an error_condition that is equivalent to a given error code. (See part 2 for the definition of equivalence.) For the HTTP errors, you can write:

class http_category_impl
: 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 choose not to override this virtual function, an error_code's default error_condition is one with the same error value and category. This is the behaviour of the default: case shown above.

Using the error codes

You can now use the http_error enumerators as error_code constants, both when setting an error:

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

and when testing for one:

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

Since the error values are based on the HTTP response codes, we can also set an error_code directly 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());

// ...
}

You can also use this technique when wrapping the errors produced by an existing library.

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

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

without needing to know the exact source of the error condition. As explained in part 2, the original error code (e.g. http_error::forbidden) is retained so that no information is lost.

In the next part, I'll show how to create and use custom error_conditions.

2 comments:

Anonymous said...

Just so you know, I find this articles very interesting cause I'm in the process of designing an error class for my personal library. I wasn't sure what to you use (an enum ?) and now I can see other options. Thanks.

CHINUX said...

Overriding the virtual function message(int ev) creates an compiler error, because enum class can not implicitly converted into int