Sunday, September 29, 2013

C++ enum.toString() and then some -- or "preprocessor metaprogramming is like, 'wow!', man"

So I have a simple enough problem. I have a series of named data objects

class Base {
const wchar_t * _name;
public:
Base(const wchar_t * name) : _name(name) {}
const wchar_t * name(void) const { return _name; }
};
template<class T>
class D : public Base {
public:
D(const wchar_t * name, const O &init) : Base(name), value(init) {}
T value;
};
view raw gistfile1.cpp hosted with ❤ by GitHub

and a container class

class Items
{
public:
Items(void);
virtual ~Items(void);
Initialise(void);
std::array<std::shared_ptr<Base>, COUNT> values;
};
view raw gistfile1.cpp hosted with ❤ by GitHub

where there are COUNT of these items, each with a name, type and default value, and the Initialise method where heap allocations are done outside the constructor -- it simply fills in the array with appropriately typed named values with their default values, and I want client code to be able to index the array with symbolic names.

Plain enums don't have a to-string (though there are tricks to auto-associate names with strings by simple preprocessor metaprogramming); to get a compile-time count there's the old stand-by of putting an extra dummy member at the end and a big fat comment about inserting new values above it.

C++11 enum classes don't give me anything over the older enums here. There are various enum-like classes out there, in varying degrees of complexity; these give type-safety (which I don't really need in this context), could be extended with extra data, can be iterated over -- but don't give a compile-time count.

Time to break out Boost for some industrial grade tooling, and be very glad that my compiler supports variadic macros.

I want to define my items one time, one place, no extras like

#define ENUMS \
((First, int, 0), \
(Second, bool, false), \
(Third, int, 42))
view raw gistfile1.cpp hosted with ❤ by GitHub

Let's start with basing our enum representation off the Boost MPL wrapper for size_t

#define MAKE_ENUM(N, NAME, T, DEFAULT) \
class NAME : public boost::mpl::size_t<N> { \
public: \
typedef T rep_type; \
static const wchar_t * const label; \
static const T default_value; \
};
view raw gistfile1.cpp hosted with ❤ by GitHub

with static initialization

#define _L(NAME) \
BOOST_PP_CAT(L, NAME)
#define INIT_ENUM_STATIC(N, NAME, T, DEFAULT) \
const wchar_t * const NAME::label = _L(BOOST_PP_STRINGIZE(NAME)); \
const T NAME::default_value = DEFAULT;
view raw gistfile1.cpp hosted with ❤ by GitHub

and from there it's just a case of using the preprocessor metaprogramming to put the bits together, just iterating over the definitions to define all the types, their static initialization and the array initialization:

#define MAKE_ENUM_I(R, DATA, N, ENUM_TUPLE) \
MAKE_ENUM(N, BOOST_PP_TUPLE_ELEM(0, ENUM_TUPLE), BOOST_PP_TUPLE_ELEM(1, ENUM_TUPLE), BOOST_PP_TUPLE_ELEM(2, ENUM_TUPLE))
#define INIT_ENUM_STATIC_I(R, DATA, N, ENUM_TUPLE) \
INIT_ENUM_STATIC(N, BOOST_PP_TUPLE_ELEM(0, ENUM_TUPLE), BOOST_PP_TUPLE_ELEM(1, ENUM_TUPLE), BOOST_PP_TUPLE_ELEM(2, ENUM_TUPLE))
#define INIT_ENUM(R, ARRAY, ELEM) \
ARRAY[ELEM()] = std::shared_ptr<D<ELEM::rep_type>>(new D<ELEM::rep_type>(ELEM::label, ELEM::default_value));
#define INIT_ENUM_T(R, ARRAY, ELEM) \
INIT_ENUM(R, ARRAY, BOOST_PP_TUPLE_ELEM(0, ELEM))
#define MAKE_ENUMS(ENUM_TUPLE) BOOST_PP_LIST_FOR_EACH_I(MAKE_ENUM_I, nullptr, BOOST_PP_TUPLE_TO_LIST(ENUM_TUPLE))
#define INIT_ENUMS(ENUM_TUPLE) BOOST_PP_LIST_FOR_EACH_I(INIT_ENUM_STATIC_I, nullptr, BOOST_PP_TUPLE_TO_LIST(ENUM_TUPLE))
#define COUNT_ENUMS(ENUM_TUPLE) BOOST_PP_TUPLE_SIZE(ENUM_TUPLE)
#define INITIALISE(ENUM_TUPLE, ARRAY) BOOST_PP_LIST_FOR_EACH(INIT_ENUM_T, ARRAY, BOOST_PP_TUPLE_TO_LIST(ENUM_TUPLE))
#define INITIALISE_ENUMS(ARRAY) INITIALISE(ENUMS, ARRAY)
view raw gistfile1.cpp hosted with ❤ by GitHub

Then in the Items.h header file where I defined D and Items, realise the common parts:

#define COUNT COUNT_ENUMS(ENUMS)
MAKE_ENUMS(ENUMS)
view raw gistfile1.cpp hosted with ❤ by GitHub

and the Items.cpp file realises the initialisations

// define enum statics
INIT_ENUMS(ENUMS)
Items::Initialise(void)
{
// default-fill array
INITIALISE_ENUMS(values)
}
view raw gistfile1.cpp hosted with ❤ by GitHub

and consumers have a tolerable symbolic constant access to the values

auto x = *(D<int> *)(items.values[Third()].get());
std::wcout << x.name() << L" = " << x.value << std::endl;
view raw gistfile1.cpp hosted with ❤ by GitHub

while extending the list only has to happen in one place.

I'm just not sure if it's a bit too magic for the purpose.

No comments :