Introduction
C++11 added a raft of new features to the C++ standard library and errors and exceptions were not left out.
In this article we will start with a quick overview of the new exception types and exception related features. While the nitty gritty details are not covered in great depth in most cases a simple usage example will be provided. The information was pulled together from various sources[1][2][3], and these, along with others, can be used to look up the in depth, detailed, specifics.
Following the lightning tour of C++11 exception support we will take a look at some further usage examples.
Example code
The example code is available with a Makefile
for building with
GNU g++ 5.4.0 and MSVC++17 project files from:
It may be useful to at least browse the source as the full example code is not always shown in the article.
For g++ each was built using the options:
-Wall -Wextra -pedantic -std=c++11 -pthread
For MSVC++17 a Win32 console application solution was created and each example source file added as a project using the default options, or with no pre-compiled header option selected if a project ended up with it turned on.
New standard library exception types
So let’s start with a brief look at the new exception types. They are:
-
std::bad_weak_ptr
(include<memory>
) -
std::bad_function_call
(include<functional>
) -
std::bad_array_new_length
(include<new>
) -
std::future_error
(include<future>
) -
std::system_error
(include<system_error>
) -
std::nested_exception
(include<exception>
)
Additionally, since C++11 std::ios_base::failure
is derived from
std::system_error
.
std::bad_weak_ptr
and std::bad_function_call
are derived from
std::exception
.
std::bad_weak_ptr
is thrown by the std::shared_ptr
constructor that takes
a std::weak_ptr
as an argument if the std::weak_ptr::expired
operation
returns true
, thus:
std::weak_ptr<int> int_wptr;
assert( int_wptr.expired() );
try
{
std::shared_ptr<int> int_sptr{ int_wptr };
}
catch ( std::bad_weak_ptr & e )
{
std::cerr << "Expired or default initialised weak_ptr: " << e.what() << "\n";
}
Which should produce output similar to:
Expired or default initialised weak_ptr: bad_weak_ptr
If a std::function
object has no target std::bad_function_call
is thrown by
std::function::operator()
, like so:
std::function<void()> fn_wrapper;
assert( static_cast<bool>(fn_wrapper)==false );
try
{
fn_wrapper();
}
catch ( std::bad_function_call & e )
{
std::cerr << "Function wrapper is empty: " << e.what() << "\n";
}
With expected output:
(g++): Function wrapper is empty: bad_function_call (msvc++): Function wrapper is empty: bad function call
std::bad_array_new_length
is derived from std::bad_alloc
, which means
it will be caught by existing catch clauses that handle std::bad_alloc
exceptions. It is thrown if an array size passed to new
is invalid by being
negative, exceeding an implementation defined size limit, or is less than
the number of provided initialiser values (MSVC++17 does not seem to
handle this case). It should be noted that this only applies to the first
array dimension as this is the only one that can be dynamic thus any other
dimension size values can be checked at compile time. In fact MSVC++17
is able to check the validity of simple literal constant size values for
the first dimension as well during compilation, hence the use of the
len
variable with an illegal array size value in the following example:
int len=-1; // negative length value
try
{
int * int_array = new int[len];
delete [] int_array;
}
catch ( std::bad_array_new_length & e )
{
std::cerr << "Bad array length: " << e.what() << "\n";
}
When run we should see output like so:
(g++): Bad array length: std::bad_array_new_length (msvc++): Bad array length: bad array new length
std::future_error
is derived from std::logic_error
and is used to report
errors in program logic when using futures and promises, for example trying
to obtain a future object from a promise object more than once viz:
std::promise<int> int_vow;
auto int_future = int_vow.get_future();
try
{
int_future = int_vow.get_future();
}
catch ( std::future_error & e )
{
std::cerr << "Error from promise/future: " << e.what() << "\n";
}
Which should display:
(g++): Error from promise/future: std::future_error: Future already retrieved (msvc++): Error from promise/future: future already retrieved
std::system_error
is derived from std::runtime_error
and is used to
report operating system errors, either directly by our code or raised by other
standard library components, as in this example:
try
{
std::thread().detach(); // Oops no thread to detach
}
catch ( std::system_error & e )
{
std::cerr << "System error from thread detach: " << e.what() << "\n";
}
Which on running should produce output along the lines of:
(g++): System error from thread detach: Invalid argument (msvc++): System error from thread detach: invalid argument: invalid argument
std::nested_exception
is not derived from anything. It is a polymorphic mixin
class that allows exceptions to be nested within each other. There is more
on nested exceptions below in the Nesting exceptions section.
Collecting, passing and re-throwing exceptions
Since C++11 there has been the capability to obtain and store a pointer
to an exception and to re-throw an exception referenced by such a pointer.
One of the main motivations for exception pointers was to be able to
transport exceptions between threads, as in the case of std::promise
and std::future
when there is an exceptional result set on the promise.
Exception pointers are represented by the std::exception_ptr
standard
library type. It is in fact a type alias to some unspecified nullable,
shared-ownership smart pointer like type that ensures any pointed to
exception remains valid while at least one std::exception_ptr
object is
pointing to it. Instances can be passed around, possibly across thread
boundaries. Default constructed instances are null pointers that compare
equal to nullptr
and test false.
std::exception_ptr
instances must be set to point to exceptions that have
been thrown, caught, and captured with std::current_exception
which
returns a std::exception_ptr
. Should we happen to have an exception object
to hand already, then we can pass it to std::make_exception_ptr
and get a std::exception_ptr
in return. std::make_exception_ptr
behaves
as if it throws, catches and captures the passed exception via
std::current_exception
.
Once we have a std::exception_ptr
it can be passed to
std::rethrow_exception()
from within a try-block to re-throw the
exception it refers to.
The example below shows passing an exception simply via a (shared) global
std::exception_ptr
object from a task thread to the main thread:
#include <exception>
#include <thread>
#include <iostream>
#include <cassert>
std::exception_ptr g_stashed_exception_ptr;
void bad_task()
{
try
{
std::thread().detach(); // Oops !!
}
catch ( ... )
{
g_stashed_exception_ptr = std::current_exception();
}
}
int main()
{
assert( g_stashed_exception_ptr == nullptr );
assert( !g_stashed_exception_ptr );
std::thread task( bad_task );
task.join();
assert( g_stashed_exception_ptr != nullptr );
assert( g_stashed_exception_ptr );
try
{
std::rethrow_exception( g_stashed_exception_ptr );
}
catch ( std::exception & e )
{
std::cerr << "Task failed exceptionally: " << e.what() << "\n";
}
}
When built and run it should output:
(g++): Task failed exceptionally: Invalid argument (msvc++): Task failed exceptionally: invalid argument: invalid argument
Of course using global variables is questionable at best so in real code you
would probably use other means, such as hiding the whole mechanism by using
std::promise
and std::future
.
Error categories, codes and conditions
std::system_error
and std::future_error
allow specifying an error code or
error condition during construction. Additionally std::system_error
can also
be constructed using an error category value.
In short error codes are lightweight objects encapsulating possibly
implementation specific error code values while error conditions are effectively
portable error codes. Error codes are represented by std::error_code
objects
while error conditions are represented by std::error_condition
objects.
Error categories define the specific error-code, error-condition mapping and
hold the error description strings for each specific error category. They are
represented by the base class std::error_category
, from which specific
error category types derive. There are several categories defined by the
standard library whose error category objects are accessed through the
following functions:
-
std::generic_category
for POSIXerrno
error conditions -
std::system_category
for errors reported by the operating system -
std::iostream_category
for IOStream error codes reported viastd::ios_base::failure
(remember since C++11std::ios_base::failure
is derived fromstd::system_error
) -
std::future_category
for future and promise related error codes provided bystd::future_error
Each function returns a const std::error_category&
to a static instance of
the specific error category type.
The standard library also defines enumeration types providing nice to use names for error codes or conditions for the various error categories:
-
std::errc
defines portable error condition values corresponding to POSIX error codes -
std::io_errc
defines error codes reported by IOStreams viastd::ios_base::failure
-
std::future_errc
defines error codes reported bystd::future_error
Each of the enumeration types have associated std::make_error_code
and std::make_error_condition
function overloads that convert a passed
enumeration value to a std::error_code
or std::error_condition
. They
also have an associated is_error_condition_enum
or is_error_code_enum
class
specialisation to aid in identifying valid enumeration error condition or code
types that are eligible for automatic conversion to std::error_condition
or std::error_code
.
Initially, from C++11, std::future_error
exceptions were constructed from
std::error_code
values. However since C++17 they are constructed
directly from std::future_errc
enumeration values.
Nesting exceptions
At the end of the New standard library exception types
section above was a brief description of std::nested_exception
which can be
used to allow us to nest one exception within another (and another, and another
and so on if we so desire). This section takes a closer look at the support for
handling nested exceptions.
While it is possible to use std::nested_exception
directly it is almost always
going to be easier to use the C++ standard library provided support.
To create and throw a nested exception we call std::throw_with_nested
,
passing it an rvalue reference to the outer exception object. That is, it is
easiest to pass a temporary exception object to std::throw_with_nested
.
std::throw_with_nested
will call std::current_exception
to obtain the
inner nested exception, and hence should be called from within the catch
block that handles the inner nested exception.
Should we catch an exception that could be nested then we can re-throw the
inner nested exception by passing the exception to std::rethrow_if_nested
.
This can be called repeatedly, possibly recursively, until the inner most
nested exception is thrown where upon the exception is no longer nested and
so std::rethrow_if_nested
does nothing.
Each nested exception thrown by std::throw_with_nested
is publicly derived
from both the type of the outer exception passed to std::throw_with_nested
and std::nested_exception
, and so has an is-a relationship with both the
outer exception type and std::nested_exception
. Hence nested exceptions can
be caught by catch blocks that would catch the outer exception type, which is
handy.
The following example demonstrates throwing nested exceptions and recursively
logging each to std::cerr
:
#include <exception>
#include <string>
#include <iostream>
#include <stdexcept>
void log_exception( std::exception const & e, unsigned level = 0u )
{
const std::string indent( 3*level, ' ' );
const std::string prefix( indent + (level?"Nested":"Outer") + " exception: " );
std::cerr << prefix << e.what() << "\n";
try
{
std::rethrow_if_nested( e );
}
catch ( std::exception const & ne )
{
log_exception( ne, level + 1 );
}
catch( ... ) {}
}
void sub_task4()
{ // do something which fails...
throw std::overflow_error{ "sub_task4 failed: calculation overflowed" };
}
void task2()
{
try
{ // pretend sub tasks 1, 2 and 3 are performed OK...
sub_task4();
}
catch ( ... )
{
std::throw_with_nested
( std::runtime_error{ "task2 failed performing sub tasks" } );
}
}
void do_tasks()
{
try
{ // pretend task 1 performed OK...
task2();
}
catch ( ... )
{
std::throw_with_nested
( std::runtime_error{ "Execution failed performing tasks" } );
}
}
int main()
{
try
{
do_tasks();
}
catch ( std::exception const & e )
{
log_exception( e );
}
}
The idea is that the code is performing some tasks and each task performs
sub-tasks. The initial failure is caused by sub-task 4 of task 2 in the
sub_task4()
function. This is caught and re-thrown nested within a
std::runtime_error
exception by the task2()
function which is then caught
and re-thrown nested with another std::runtime_error
by the do_tasks
function. This composite nested exception is caught and logged in main by calling
log_exception
, passing it the caught exception reference.
log_exception
first builds and outputs to std::cerr
a log message for the
immediate, outer most exception. It then passes the passed in exception
reference to std::rethrow_if_nested
within a try-block. If this throws, the
exception had an inner nested exception which is caught and passed recursively
to log_exception
. Otherwise the exception was not nested, no inner exception
is re-thrown and log_exception
just returns.
When built and run the program should produce:
Outer exception: Execution failed performing tasks Nested exception: task2 failed performing sub tasks Nested exception: sub_task4 failed: calculation overflowed
Detecting uncaught exceptions
C++98 included support for detecting if a thread has a live exception in
flight with the std::uncaught_exception
function. A live exception is one
that has been thrown but not yet caught (or entered std::terminate
or
std::unexpected
). The std::uncaught_exception
function returns true
if
stack unwinding is in progress in the current thread.
It turns out that std::uncaught_exception
is not usable for what would
otherwise be one of its main uses: knowing if an object’s destructor is being
called due to stack unwinding, as detailed in Herb Sutter’s N4152 'uncaught
exceptions' paper to the ISO C++ committee[4]. For this scenario knowing the
number of currently live exceptions in a thread is required not just knowing
if a thread has at least one live exception.
If an object’s destructor only knows if it is being called during stack unwinding it cannot know if it is because of an exception thrown after the object was constructed, and so needs exceptional clean-up (e.g. a rollback operation), or if it was due to stack unwinding already in progress and it was constructed as part of the clean-up and so probably not in an error situation itself. To fix this an object needs to collect and save the number of uncaught exceptions in flight at its point of construction and during destruction compare this value to the current value and only take exceptional clean-up action if the two are different.
So from C++17 std::uncaught_exception
has been deprecated in favour of
std::uncaught_exceptions
(note the plural, "s", at the end of the name)
which returns an int
value indicating the number of live exceptions in the
current thread.
Some additional usage scenarios
Now we have had a whizz around the new C++11 exceptions and exception features let’s look at some other uses.
Centralising exception handling catch
blocks
Have you ever written code where you wished that common exception handling blocks could be pulled out to a single point? If so then read on.
The idea is to use a catch all catch (…)
clause containing a call to
std::current_exception
to obtain a std::exception_ptr
which can then be
passed to a common exception processing function where it is re-thrown and the
re-thrown exception handled by a common set of catch clauses.
Using the simple C API example shown below:
extern "C"
{
struct widget;
enum status_t { OK, no_memory, bad_pointer, value_out_of_range, unknown_error };
status_t make_widget( widget ** ppw, unsigned v );
status_t get_widget_attribute( widget const * pcw, unsigned * pv );
status_t set_widget_attribute( widget * pw, unsigned v );
status_t destroy_widget( widget * pw );
}
Which allows us to make widgets with an initial value, get and set the
attribute value and destroy widgets when done with them. Each API function
returns a status code meaning a C++ implementation has to convert any
exceptions to status_t
return code values. The API could be exercised
like so:
int main( void )
{
struct widget * pw = NULL;
assert(make_widget(NULL, 19u) == bad_pointer);
assert(make_widget(&pw, 9u) == value_out_of_range);
if (make_widget(&pw, 45u) != OK)
return EXIT_FAILURE;
unsigned value = 0u;
assert(get_widget_attribute(pw, &value) == OK);
assert(get_widget_attribute(NULL, &value) == bad_pointer);
assert(value == 45u);
assert(set_widget_attribute(pw, 67u) == OK);
assert(set_widget_attribute(NULL, 11u) == bad_pointer);
assert(set_widget_attribute(pw, 123u) == value_out_of_range);
get_widget_attribute(pw, &value);
assert(value == 67u);
assert(destroy_widget(pw) == OK);
assert(destroy_widget(NULL) == bad_pointer);
}
We could imagine a simple quick and dirty C++ implementation like so:
namespace
{
void check_pointer( void const * p )
{
if ( p==nullptr )
throw std::invalid_argument("bad pointer");
}
}
extern "C"
{
struct widget
{
private:
unsigned attrib = 10u;
public:
unsigned get_attrib() const { return attrib; }
void set_attrib( unsigned v )
{
if ( v < 10 || v >= 100 )
throw std::range_error
( "widget::set_widget_attribute: attribute value out of range [10,100)" );
attrib = v;
}
};
status_t make_widget( widget ** ppw, unsigned v )
{
status_t status{ OK };
try
{
check_pointer( ppw );
*ppw = new widget;
(*ppw)->set_attrib( v );
}
catch ( std::invalid_argument const & )
{
return bad_pointer;
}
catch ( std::bad_alloc const & )
{
status = no_memory;
}
catch ( std::range_error const & )
{
status = value_out_of_range;
}
catch ( ... )
{
status = unknown_error;
}
return status;
}
status_t get_widget_attribute( widget const * pcw, unsigned * pv )
{
status_t status{ OK };
try
{
check_pointer( pcw );
check_pointer( pv );
*pv = pcw->get_attrib();
}
catch ( std::invalid_argument const & )
{
return bad_pointer;
}
catch ( ... )
{
status = unknown_error;
}
return status;
}
status_t set_widget_attribute( widget * pw, unsigned v )
{
status_t status{ OK };
try
{
check_pointer( pw );
pw->set_attrib( v );
}
catch ( std::invalid_argument const & )
{
return bad_pointer;
}
catch ( std::range_error const & )
{
status = value_out_of_range;
}
catch ( ... )
{
status = unknown_error;
}
return status;
}
status_t destroy_widget( widget * pw )
{
status_t status{ OK };
try
{
check_pointer( pw );
delete pw;
}
catch ( std::invalid_argument const & )
{
return bad_pointer;
}
catch ( ... )
{
status = unknown_error;
}
return status;
}
}
Not to get hung up on the specifics of the implementation and that I have
added a check_pointer
function to convert bad null pointer arguments to
exceptions just for them to be converted to a status code, we see that the
error handling in each API function is larger than the code doing the work,
which is not uncommon.
Using std::current_exception
, std::exception_ptr
and
std::rethrow_exception
allows us to pull most of the error handling into a
single function, thus:
namespace
{
status_t handle_exception( std::exception_ptr ep )
{
try
{
std::rethrow_exception( ep );
}
catch ( std::bad_alloc const & )
{
return no_memory;
}
catch ( std::range_error const & )
{
return value_out_of_range;
}
catch ( std::invalid_argument const & )
{// for simplicity we assume all bad arguments are bad pointers
return bad_pointer;
}
catch ( ... )
{
return unknown_error;
}
}
}
Now each function’s try
block only requires a catch (…)
clause to capture
the exception and pass it to the handling function, for example the
set_widget_attribute
implementation becomes:
status_t set_widget_attribute( widget * pw, unsigned v )
{
status_t status{ OK };
try
{
check_pointer( pw );
pw->set_attrib( v );
}
catch ( ... )
{
status = handle_exception( std::current_exception() );
}
return status;
}
We can see that the implementation is shorter and more importantly no longer
swamped by fairly mechanical and repetitive error handling code translating
exceptions into error codes, all of which is now performed in the common
handle_exception
function.
We can reduce the code clutter even more, at the risk of potentially greater code generation and call overhead on the good path, by using the execute-around pattern[5] (more common in languages like Java and C#) combined with lambda functions. (thanks to Steve Love[6] for mentioning execute around to me at the ACCU London August 2017 social evening).
The idea is to move the work-doing part of each function, previously the code in each of the API functions' try-block, to its own lambda function and pass an instance of this lambda to a common function that will execute the lambda within a try block which has the common exception catch handlers as in the previous incarnation. As each lambda function in C++ is a separate, unique, type we have to use a function template, parametrised on the (lambda) function type, viz:
template <class FnT>
status_t try_it( FnT && fn )
{
try
{
fn();
}
catch ( std::bad_alloc const & )
{
return no_memory;
}
catch ( std::range_error const & )
{
return value_out_of_range;
}
catch (std::invalid_argument const & )
{// for simplicity we assume all bad arguments are bad pointers
return bad_pointer;
}
catch ( ... )
{
return unknown_error;
}
return OK;
}
The form of each API function implementation is now shown by the third
incarnation of the set_widget_attribute
implementation:
status_t set_widget_attribute( widget * pw, unsigned v )
{
return try_it( [pw, v]() -> void
{
check_pointer( pw );
pw->set_attrib( v );
}
);
}
Using nested exceptions to inject additional context information
As I hope was apparent from the Nesting exceptions section above nested exceptions allow adding additional information to an originally thrown (inner most) exception as it progresses through stack unwinding.
Of course doing so for every stack frame is possible but very tedious and probably overkill. On the other hand there are times when having some additional context can really aid tracking down a problem.
One area I have found that additional context is useful is threads. You have an application, maybe a service or daemon, that throws an exception in a worker thread. You have carefully arranged for such exceptions to be captured at the thread function return boundary and set them on a promise so a monitoring thread (maybe the main thread) that holds the associated future can re-throw the exception and take appropriate action which always includes logging the problem.
You notice that an exception occurs in the logs, it is a fairly generic
problem - maybe a std::bad_alloc
or some such. At this point you are
wondering which thread it was that raised the exception. You go back to your
thread wrapping code and beef up the last-level exception handling to wrap
any exception in an outer exception that injects the thread’s contextual
information and hand a std::exception_ptr
to the resultant nested
exception to the promise object.
The contextual information could include the thread ID and maybe a task name. If the thread is doing work on behalf of some message or event then such details should probably be added to the outer exception’s message as these will indicate what the thread was doing.
Of course the thread exit/return boundary is not the only place such contextual information can be added. For example in the event case mentioned above it may be that adding the message / event information is better placed in some other function. In this case you may end up with a three-level nest exception set: the original inner most exception, the middle event context providing nested exception and the outer thread context providing nested exception.
Error codes of your very own
I saw the details of this usage example explained quite nicely by a blog post of Andrzej Krzemieński[7] that was mentioned on ISO Cpp[8].
The cases where this is relevant are those where a project has sets of error values, commonly represented as enumeration values. Large projects may have several such enumeration types for different subsystems and the enumeration values they employ may overlap. For example, we might have some error values from a game’s application engine and its renderer sub-system:
namespace the_game
{
enum class appengine_error
{ no_object_index = 100
, no_renderer
, null_draw_action = 200
, bad_draw_context = 300
, bad_game_object
, null_player = 400
};
}
namespace the_game
{
enum class renderer_error
{ game_dimension_too_small = 100
, game_dimension_bad_range
, board_too_small = 200
, board_bad_range
, game_dimension_bad
, board_not_square = 300
, board_size_bad
, bad_region = 400
, cell_coordinate_bad = 500
, new_state_invalid
, prev_state_invalid
};
}
Note
|
The error types and values were adapted from panic value types from a simple noughts and crosses (tic tac toe) game I wrote with a friend more than a decade ago with the goal of learning a bit about Symbian mobile OS development. |
In such cases we can either deal in the enumeration types directly when such
error values are passed around with the effect that the various parts of the
project need access to the definitions of each enumeration type they come into
contact with. Or we can use a common underlying integer type, such as int
for passing around such error value information and lose the ability to
differentiate between errors from different subsystems or domains that share
the same value.
Note
|
It would be possible to use different underlying types for each of the various error value sets but there are only so many and such an approach seems fragile at best given the ease with which C++ converts/promotes between fundamental types and the need to ensure each enumeration uses a different underlying type. |
If only C++ had an error code type as standard that would allow us to both
traffic in a single type for error values and allow us to differentiate
between different sets of errors that may use the same values. If we could
also assign a name for each set and text descriptions for each error value that
would be icing on the cake. Oh, wait, it does: std::error_code
. We just have
to plug our own error value enumeration types into it. The only caveats are
that all the underlying values be correctly convertible to int
and that
our custom error types must reserve an underlying value of 0 to mean OK,
no error. Even if our error value types do not provide an OK enumeration
value of 0 explicitly so long as a value of 0 is not reserved for an error
value then we can always create a zero valued instance of the error enum:
the_game::appengine_error ok_code_zero_value{};
Different error value sets or domains are called error categories by the C++ standard library and to completely define an error code we require an {error value, error category} pair.
To create our own error categories we define a specialisation of
std::error_category
for each error value set we have. To keep
std::error_code
lightweight it does not store a std::error_category
object within each instance. Rather each std::error_category
specialisation has a single, static, instance. std::error_code
objects
contain the error value and a reference (pointer) to the relevant
std::error_category
specialisation static instance. Because all references
to an error category type instance refer to the same, single instance of that
type, the object address can be used to uniquely identify and differentiate
each specific error category and allows std::error_code
objects to be
compared.
Each std::error_category
specialisation provides overrides of the name
and message
pure virtual member functions. The name
member function
returns a C-string representing the name of the category. The message
member function returns a std::string
describing the passed in category
error value (passed as an int
). For example an error category type for the
the_game::appengine_error
error values might look like so:
struct appengine_error_category : std::error_category
{
const char* name() const noexcept override;
std::string message(int ev) const override;
};
const char* appengine_error_category::name() const noexcept
{
return "app-engine";
}
std::string appengine_error_category::message( int ev ) const
{
using the_game::appengine_error;
switch( static_cast<appengine_error>(ev) )
{
case appengine_error::no_object_index:
return "No object index";
case appengine_error::no_renderer:
return "No renderer currently set";
case appengine_error::null_draw_action:
return "Null draw action pointer";
case appengine_error::bad_draw_context:
return "Draw action context has null graphics context or renderer pointer";
case appengine_error::bad_game_object:
return "Draw action context has null game object pointer";
case appengine_error::null_player:
return "Current player pointer is null";
default:
return "?? unrecognised error ??";
}
}
To create std::error_code
values from a custom error (enumeration) value
in addition to the std::error_category
specialisation we need to provide
two other things. First, an overload of std::make_error_code
that takes our
error value type as a parameter and returns a std::error_code
constructed
from the passed error value and the static std::error_category
specialisation object. This should be in the same namespace as our error value
enum type.
In this use case the std::make_error_code
function overload is the only
thing that requires access to the custom error category static instance. As
such we can define the static object to be local to the std::make_error_code
function overload, as in the following example:
namespace the_game
{
std::error_code make_error_code(appengine_error e)
{
static const appengine_error_category theappengine_error_categoryObj;
return {static_cast<int>(e), theappengine_error_categoryObj};
}
}
As the std::make_error_code
function overload definition is the only thing
that requires the definition of the std::error_category
specialisation it
is probably best if they are both placed in the same implementation file. The
declaration can be placed in the same header as the custom error value
enumeration type definition as it will be used when converting such values to
std::error_code
instances - the appengine_error.h
header for the
appengine_error
example case.
Second, we need to provide a full specialisation of the
std::is_error_code_enum
struct template, specifying our error code type as
the template parameter. The easiest implementation is to derive from
std::true_type
and have an empty definition. This should be in the
std
namespace, one of the few things application code can add to std
.
Below is the std::is_error_code_enum
specialisation for
the_game::appengine_error
:
namespace std
{
using the_game::appengine_error;
template <> struct is_error_code_enum<appengine_error> : true_type {};
}
It is also probably best placed in the same header as the custom error values enumeration type definition.
Subsystem API (member) functions can then pass around std::error_code
instances rather than specific enumeration types or simple integer values that
loose the category information. Producers of such error codes need to
include both system_error
for std::error_code
and the header containing
the error value enum definition, along with the std::make_error_code
overload declaration (only) and the std::is_error_code_enum
struct
template specialisation definition. So to produce std::error_code
objects
from the_game::appengine_error
values the previously mentioned
appengine_error.h
header would need to be included.
Consumers need only include system_error
for std::error_code
and will
still be able to access the error value, category name and error value
description string.
For example some spoof game appengine
implementation code for
updating the game board might complain if it does not have an associated
renderer object to pass on the request to by returning a
the_game::appengine_error::no_renderer
error converted to a std::error_code
:
std::error_code appengine::update_game_board()
{ // good case demonstrates zero-initialising enum class instance
return rp_ ? appengine_error{} : appengine_error::no_renderer;
}
It thus needs to include the appengine_error.h
header and well as
system_error
. However, the caller of this member function only sees the
returned std::error_code
, and so only needs to include system_error
, as
well as any appengine
API headers of course. This is demonstrated by this
simple spoof usage program:
#include "custom_error_code_bits/the_game_api.h"
#include <system_error>
#include <iostream>
#include <string>
void log_bad_status_codes( std::error_code ec )
{
if ( ec )
std::clog << ec << " " << ec.message() << "\n";
}
int main()
{
auto & engine{ the_game::get_appengine() };
// Should fail as setting renderer supporting invalid dimension range
std::unique_ptr<the_game::renderer> rend{new the_game::oops_renderer};
log_bad_status_codes( engine.take_renderer( std::move(rend) ) );
// Should fail as no renderer successfully set to draw board
log_bad_status_codes( engine.update_game_board() );
// OK - nothing to report, this renderer is fine and dandy
rend.reset( new the_game::fine_renderer );
log_bad_status_codes( engine.take_renderer( std::move(rend)) );
// OK - now have renderer to render board updates
log_bad_status_codes( engine.update_game_board() );
}
Which shows spoof usage for both converted the_game::renderer_error
values and
the the_game::appengine_error
values I have shown examples of. When built
and run the output should be:
renderer:101 Reported max. supported game grid less than the min. app-engine:101 No renderer currently set
Of course this is all about error values, codes and categories, nothing about
exceptions (other than returning std::error_code
values could allow
functions to be marked noexcept
). Remember however that we can always
construct a std::system_error
exception object from a std::error_code
object.
References
-
The C++ Standard Library, second edition, Nicolai M. Josuttis
-
N3337, post C++11 Working Draft, Standard for Programming Language C++ (PDF), ISO WG21
-
See for example the stackoverflow question and answers What is the “Execute Around” idiom?, Roman C, Jon Skeet et al