C++11 introduced many new exception related features. Ralph McArdell gives a comprehensive overview.
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 – [ cppreference ], [ Josuttis12 ], [ N3337 ] – 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:
https://github.com/ralph-mcardell/article-cxx11-exception-support-examples
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, starting with 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 (see Listing 1), which should produce output similar to:
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"; } |
Listing 1 |
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()
(see Listing 2)
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"; } |
Listing 2 |
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 example in Listing 3.
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"; } |
Listing 3 |
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 (see Listing 4), which should display:
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"; } |
Listing 4 |
(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 the example in Listing 5, which on running should produce output along the lines of:
try{ std::thread().detach(); // Oops, no thread to } // detach catch ( std::system_error & e ){ std::cerr << "System error from thread detach: " << e.what() << "\n"; } |
Listing 5 |
(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 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 in Listing 6 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"; } } |
Listing 6 |
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
(which, if you remember, has been derived fromstd::system_error
since C++11) -
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
.
In C++11 and C++14,
std::future_error
exceptions are constructed from
std::error_code
values. However, from 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 example in Listing 7 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 ); } } |
Listing 7 |
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 [
N4152
]. 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 in Listing 8 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 as shown in Listing 9.
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 ); } |
Listing 8 |
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); } |
Listing 9 |
We could imagine a simple quick and dirty C++ implementation like Listing 10.
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; } } |
Listing 10 |
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 (Listing 11).
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; } } } |
Listing 11 |
Now each function’s
try
block only requires a
catch (…)
clause to capture the exception and pass it to the handling function and, for example, the
set_widget_attribute
implementation becomes Listing 12.
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; } |
Listing 12 |
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 [ Vijayakumar16 ] (more common in languages like Java and C#) combined with lambda functions. (thanks to Steve Love [ Love ] 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 (Listing 13).
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; } |
Listing 13 |
The form of each API function implementation is now shown by the third incarnation of the
set_widget_attribute
implementation (Listing 14).
status_t set_widget_attribute ( widget * pw, unsigned v ){ return try_it( [pw, v]() -> void{ check_pointer( pw ); pw->set_attrib( v ); } ); } |
Listing 14 |
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 Krzemienski [ Krzemienski17 ] that was mentioned on ISO Cpp [ ISO ].
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 (Listing 15 and Listing 16).
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 }; } |
Listing 15 |
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 }; } |
Listing 16 |
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’s 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 Listing 17.
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 ??"; } } |
Listing 17 |
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 Listing 18.
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}; } } |
Listing 18 |
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
. Listing 19 shows 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 {}; } |
Listing 19 |
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
(Listing 20).
std::error_code appengine::update_game_board(){ // good case demonstrates zero-initialising // enum class instance return rp_ ? appengine_error{} : appengine_error::no_renderer; } |
Listing 20 |
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 the simple spoof usage program in Listing 21, 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:
#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() ); } |
Listing 21 |
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
[cppreference] http://en.cppreference.com
[ISO] Cpp: https://isocpp.org/
[Josuttis12] Josuttis, Nicolai M. (2012) The C++ Standard Library , second edition, Addison Wesley Longman
[Krzemienski17] Your own error code, Andrzej Krzemienski: https://akrzemi1.wordpress.com/2017/07/12/your-own-error-code/
[Love] Steve Love: https://uk.linkedin.com/in/steve-love-1198994
[N3337] Post C++11 Working Draft, Standard for Programming Language C++
[N4152] Herb Sutter, uncaught_exceptions: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4152.pdf
[Vijayakumar16] Design Patterns – Execute Around Method Pattern: http://www.karthikscorner.com/sharepoint/design-patterns-execute-around-method-pattern/