Friday, April 09, 2010

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

[ part 1, part 2 ]

Enumerators as class constants

As we have seen, the <system_error> header defines enum class errc:

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

the enumerators of which are placeholders for error_condition constants:

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

Obviously, this is because there's an implicit conversion from errc to error_condition using a single-argument constructor. Simple. Right?

It's not quite that simple

There's a few reasons why there's a bit more to it than that:

  • The enumerator provides an error value, but to construct an error_condition we need to know the category too. The <system_error> facility uses categories to support multiple error sources, and a category is an attribute of both error_code and error_condition.
  • The facility should be user-extensible. That is, users (as well as future extensions to the standard library) need to be able to define their own placeholders.
  • The facility should support placeholders for either error_code or error_condition. Although enum class errc provides placeholders for error_condition constants, other use cases may require constants of type error_code.
  • Finally, it should allow explicit conversion from an enumerator to error_code or error_condition. Portable programs may need to create error codes that are derived from the std::errc::* enumerators.

So, while it's true that the line:

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

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

Step 1: determine whether the enum is an error code or condition

Two type traits are used to register enum types with the facility:

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 may be implicitly converted to an 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 for neither conversion (hence the use of false_type above), but enum class errc is registered as follows:

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

The implicit conversion is accomplished by conditionally enabled conversion constructors. This is probably implemented using SFINAE, but for simplicity you need only 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 (ec == std::errc::file_exists)

the compiler has to choose 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 chooses the latter because the error_condition conversion constructor is available, but the error_code one is not.

Step 2: associate the value with an error category

An error_condition object contains two attributes: value and category. Now that we're in the constructor, these need to be initialised correctly.

This is accomplished by having the constructor call the function make_error_condition(). To enable user-extensibility, this function is located using argument-dependent lookup. Of course, since errc is in namespace std, its make_error_condition() is found there too.

The implementation of make_error_condition() is simply:

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

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

If we were in the error_code conversion constructor (for an appropriately registered enum type), the function called would be make_error_code(). In other respects, the construction of error_code and error_condition is the same.

Explicit conversion to error_code or error_condition

Although error_code is primarily intended for use with OS-specific errors, portable code may want to construct an error_code from an errc enumerator. For this reason, both make_error_code(errc) and make_error_condition(errc) are provided. Portable code can use these 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
}

Some history

The original <system_error> proposal defined error_code constants 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 size overhead of so many global objects, and an alternative approach was requested. We researched the possibility of using constexpr, but that was ultimately found to be incompatible with some other aspects of the <system_error> facility. This left conversion from enum as the best available design.

Next, I'll start showing how you can extend the facility to add your own error codes and conditions.

3 comments:

Anne said...

This would be great if it were used in boost::asio! Unfortunately, it does not appear to be. From boost/asio/error.hpp:


template<> struct is_error_code_enum<boost::asio::error::basic_errors>
{
static const bool value = true;
};


I can't find a set of error conditions I can use for asio errors; just error codes.

Anonymous said...

In Part-3, the paragraph: "Obviously, this is because there's an implicit conversion from errc to error_condition using a single-argument constructor. Simple. Right?"

Should not say "because there's an implicit conversion from errc to error_code" instead?

Anonymous said...

I don't think so, recall that a single error_condition can map to several error_codes, and since there is a one-to-one mapping between error_conditions and errc enumerators, an error_condition cannot be converted to a single error_code.