C++11 (and beyond) Exception Support

Wednesday, 27 Sep 2017 - 16:26 +0100| Tags: code, C++

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 POSIX errno error conditions

  • std::system_category for errors reported by the operating system

  • std::iostream_category for IOStream error codes reported via std::ios_base::failure (remember since C++11 std::ios_base::failure is derived from std::system_error)

  • std::future_category for future and promise related error codes provided by std::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 via std::ios_base::failure

  • std::future_errc defines error codes reported by std::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.