Plugging a library into boost::system::error_code

See here for this guide, but for std::error_code.

This section illustrates how you can hook into the boost::system::error_code system from the Boost in order to work with your own set of error codes. As is usually the case in C++, doing this is straightforward but requires typing boilerplate to tell Boost.System about your custom error type. This is not part of Outcome library, but we still provide this short guide here, because how to do this is not well documented [1].

Suppose you want to report all reasons for failure in converting a std::string to a non-negative int. The list is:

#include <boost/system/error_code.hpp>  // bring in boost::system::error_code et al
#include <iostream>
#include <string>  // for string printing

// This is the custom error code enum
enum class ConversionErrc
{
  Success = 0,  // 0 should not represent an error
  EmptyString = 1,
  IllegalChar = 2,
  TooLong = 3,
};

namespace boost
{
  namespace system
  {
    // Tell the C++ 11 STL metaprogramming that enum ConversionErrc
    // is registered with the standard error code system
    template <> struct is_error_code_enum<ConversionErrc> : std::true_type
    {
    };
  }  // namespace system
}  // namespace boost

namespace detail
{
  // Define a custom error code category derived from boost::system::error_category
  class ConversionErrc_category : public boost::system::error_category
  {
  public:
    // Return a short descriptive name for the category
    virtual const char *name() const noexcept override final { return "ConversionError"; }
    // Return what each enum means in text
    virtual std::string message(int c) const override final
    {
      switch(static_cast<ConversionErrc>(c))
      {
      case ConversionErrc::Success:
        return "conversion successful";
      case ConversionErrc::EmptyString:
        return "converting empty string";
      case ConversionErrc::IllegalChar:
        return "got non-digit char when converting to a number";
      case ConversionErrc::TooLong:
        return "the number would not fit into memory";
      default:
        return "unknown";
      }
    }
    // OPTIONAL: Allow generic error conditions to be compared to me
    virtual boost::system::error_condition default_error_condition(int c) const noexcept override final
    {
      switch(static_cast<ConversionErrc>(c))
      {
      case ConversionErrc::EmptyString:
        return make_error_condition(boost::system::errc::invalid_argument);
      case ConversionErrc::IllegalChar:
        return make_error_condition(boost::system::errc::invalid_argument);
      case ConversionErrc::TooLong:
        return make_error_condition(boost::system::errc::result_out_of_range);
      default:
        // I have no mapping for this code
        return boost::system::error_condition(c, *this);
      }
    }
  };
}  // namespace detail

// Define the linkage for this function to be used by external code.
// This would be the usual __declspec(dllexport) or __declspec(dllimport)
// if we were in a Windows DLL etc. But for this example use a global
// instance but with inline linkage so multiple definitions do not collide.
#define THIS_MODULE_API_DECL extern inline

// Declare a global function returning a static instance of the custom category
THIS_MODULE_API_DECL const detail::ConversionErrc_category &ConversionErrc_category()
{
  static detail::ConversionErrc_category c;
  return c;
}


// Overload the global make_error_code() free function with our
// custom enum. It will be found via ADL by the compiler if needed.
inline boost::system::error_code make_error_code(ConversionErrc e)
{
  return {static_cast<int>(e), ConversionErrc_category()};
}

int main(void)
{
  // Note that we can now supply ConversionErrc directly to error_code
  boost::system::error_code ec = ConversionErrc::IllegalChar;

  std::cout << "ConversionErrc::IllegalChar is printed by boost::system::error_code as "
    << ec << " with explanatory message " << ec.message() << std::endl;

  // We can compare ConversionErrc containing error codes to generic conditions
  std::cout << "ec is equivalent to boost::system::errc::invalid_argument = "
    << (ec == std::errc::invalid_argument) << std::endl;
  std::cout << "ec is equivalent to boost::system::errc::result_out_of_range = "
    << (ec == std::errc::result_out_of_range) << std::endl;
  return 0;
}
View this code on Github

This might look like a lot of extra boilerplate over simply using your custom error code enum directly, but look at the advantages:

  1. Any code which can speak boost::system::error_code can now work with errors from your code, AND without being recompiled.
  2. boost::system::system_error can now wrap your custom error codes seamlessly, allowing your custom error code to be converted into a C++ exception and back out again without losing information.
  3. boost::system::error_code knows how to print itself, and will print your custom error code without extra work from you. As usually you’d need to define a print routine for any custom error code you’d write anyway, there is actually very little extra boilerplate here.
  4. If you implement the default_error_condition() override, you can allow code exclusively written to understand boost::system::errc alone to examine your custom error code domain for equivalence to the standard error conditions, AND without being recompiled.

This documentation recommends that when you define your custom enum for representing error_codes, you should always make sure that value 0 never represents an actual error: it should either represent a success or should not be provided at all. If you only intend to use your enum inside result<> or outcome<> you can just start your enumerations from 1. If you intend to also return boost::system::error_code directly from functions, you should probably define value 0 as success, so that you are able to inform about function’s success by returning MyEnum::Success. This is because error_code’s contextual conversion to bool (which some people use to check if there was an error or not) only checks for the numeric value of the error code (without looking at error domain (category)).

[1]: The only documentation I’m aware of is the quite old guide by Chris Kohlhoff, founder of ASIO and the Networking TS: