Andrew Lipscomb
Andrew Lipscomb

Reputation: 1056

Templates Vs Macros for C++ Error Code Boilerplate

TL;DR - can a boilerplate generation macro be implemented using templates?

I have some code which makes use of the C++ std::error_code and error_category classes. I've found that once the number of error codes started to grow the amount of boilerplate being written for each was also growing very quickly.

To work around this I've written some macros which should be statically checking and generating most of the boilerplate from the things we actually care about - the enum codes and the messages attached to them. These macros take in the enum and the messages as a const std::map.

My question - can this sort of boilerplate generation be replaced with a template of some sort? Right now if this fails the goto advice for others has been "check for static_asserts" which make it a bit of a PITA to use.

If it can't be simply replaced with templates, can code be added to improve the output of a failed compile so these are less painful to use? Right now a failed compile outputs both the static asserts and lot of other unneeded output.

I've included some code below which demonstrates the macros - I've removed all of our namespace-ing related code so it may be a tad incorrect but should demonstrate the goal well enough.

//C++14 definition - we are using C++11
template< bool B, class T = void >
using enable_if_t = typename std::enable_if<B,T>::type;

//Generic template test for any other type
template <typename T, typename = void>
struct is_std_map : std::false_type {};

//Specialised test for a std::map type
template <typename T>
struct is_std_map<T, enable_if_t<
                    std::is_same<typename T::value_type,
                                std::pair<const typename T::key_type,
                                          typename T::mapped_type>
                    >::value>
> : std::true_type {};

#define MAKE_ERROR_CODE_CATEGORY(EC, EC_MESSAGE_MAP)               \
/* Check that we have an enum type as the first arg, and a const std::map arg for the second */                 \
static_assert(std::is_enum<EC>::value, "!");                                             \
static_assert(std::is_const<decltype(EC_MESSAGE_MAP)>::value, "!" );                \
static_assert(is_std_map<decltype(EC_MESSAGE_MAP)>::value, "!");        \
/* Validate that the non-const types for our EC and our EC_MESSAGE_MAP are as expected*/                        \
static_assert(std::is_same<                                                                                     \
                    std::remove_const<EC>::type,                                                                \
                    std::remove_const<decltype(EC_MESSAGE_MAP)::key_type                                        \
                >::type>::value,                                                                                \
              "!");                          \
static_assert(std::is_same<                                                                                     \
                    std::remove_const<std::string>::type,                                                       \
                    std::remove_const<decltype(EC_MESSAGE_MAP)::mapped_type                                     \
                >::type>::value,                                                                                \
              "!");  \
/*Generates a standardised category for the provided EC */                                                      \
struct EC## _category : std::error_category                                                                \
{                                                                                                               \
    const char* name() const noexcept override                                                                  \
    {                                                                                                           \
      return  #EC ;                                                                                             \
    }                                                                                                           \
    std::string message(int c) const override                                                                   \
    {                                                                                                           \
        EC code = static_cast<EC>(c);                                                                           \
        auto itr = EC_MESSAGE_MAP.find(code);                                                                   \
        if (itr != EC_MESSAGE_MAP.end())                                                                        \
        {                                                                                                       \
            return itr->second;                                                                                 \
        }                                                                                                       \
        else                                                                                                    \
        {                                                                                                       \
            return "(unrecognized error)";                                                                      \
        }                                                                                                       \
    }                                                                                                           \                                                                                                    \
};
namespace std                                                                                                   \
{                                                                                                               \
    template <>                                                                                                 \
        struct is_error_code_enum< EC > : true_type {};                                   \
}                                                                                                               \
/* Declare a global function returning a static instance of the custom category */                              \
const EC## _category& EC## _category_generator()                     \
{                                                                                                               \
    static EC## _category c;                                                              \
    return c;                                                                                                   \
}                                                                                                               \
/* Adds the standard error code construction call */                                                            \
inline std::error_code make_error_code(EC e)                                              \
{                                                                                                               \
    return {static_cast<int>(e), EC## _category_generator()};                                                   \
}                                 

Upvotes: 2

Views: 687

Answers (1)

Jarod42
Jarod42

Reputation: 217275

Your macro is needed to stringify the enum, and help to avoid boilerplate for specialization in std, but you can extract some code to create templates:

// Traits to retrieve name and mapping from enum.
template <typename E>
struct enum_traits
{
    static_assert(std::is_enum<E>::value, "!");

    static const char* const name;
    static const std::map<E, std::string> mapping;
};

template <typename E>
struct ECategory_impl : std::error_category
{
    static_assert(std::is_enum<E>::value, "!");

    const char* name() const noexcept override
    {
      return enum_traits<E>::name;
    }

    std::string message(int c) const override
    {
        const auto& Map = enum_traits<E>::mapping;
        E code = static_cast<E>(c);
        auto itr = Map.find(code);
        if (itr != Map.end())
        {
            return itr->second;
        }
        else
        {
            return "(unrecognized error)";
        }
    }
};

template <typename E>
std::error_code make_error_code(E e)
{
    static_assert(std::is_enum<E>::value, "!");
    static const ECategory_impl<E> categ{};
    return {static_cast<int>(e), categ};
}

and then the MACRO (Which might be omitted if you consider that now stuff to repeat is low enough):

#define MAKE_ERROR_CODE_CATEGORY(E)                      \
/* Stringification for the name*/                        \
template <> const char* const enum_traits<E>::name = #E; \
/* Specialization in std */                              \
namespace std                                            \
{                                                        \
    template <>                                          \
    struct is_error_code_enum<E> : true_type {};         \
}                                                        \
/* Alias for custom naming */                            \
using E##_category = ECategory_impl<E>;
//                                    ^
// You might remove that final ';' for a usage `MAKE_ERROR_CODE_CATEGORY(E);`
// instead of current `MAKE_ERROR_CODE_CATEGORY(E)`

Usage is then similar to:

enum class E {A, B};
template <>
const std::map<E, std::string> enum_traits<E>::mapping{
    {E::A, "A"},
    {E::B, "E"}
};

MAKE_ERROR_CODE_CATEGORY(E)

Demo

Upvotes: 2

Related Questions