(The approach of the better_enums library)
There is a way to do enum to string in current C++ that looks like this:
ENUM(Channel, char, Red = 1, Green, Blue)
// "Same as":
// enum class Channel : char { Red = 1, Green, Blue };
Usage:
Channel c = Channel::_from_string("Green"); // Channel::Green (2)
c._to_string(); // string "Green"
for (Channel c : Channel::_values())
std::cout << c << std::endl;
// And so on...
All operations can be made constexpr
. You can also implement the C++17 reflection proposal mentioned in the answer by @ecatmur.
- There is only one macro. I believe this is the minimum possible, because preprocessor stringization (
#
) is the only way to convert a token to a string in current C++.
- The macro is pretty unobtrusive – the constant declarations, including initializers, are pasted into a built-in enum declaration. This means they have the same syntax and meaning as in a built-in enum.
- Repetition is eliminated.
- The implementation is most natural and useful in at least C++11, due to
constexpr
. It can also be made to work with C++98 + __VA_ARGS__
. It is definitely modern C++.
The macro's definition is somewhat involved, so I'm answering this in several ways.
- The bulk of this answer is an implementation that I think is suitable for the space constraints on StackOverflow.
- There is also a CodeProject article describing the basics of the implementation in a long-form tutorial. [Should I move it here? I think it's too much for a SO answer].
- There is a full-featured library "Better Enums" that implements the macro in a single header file. It also implements N4428 Type Property Queries, the current revision of the C++17 reflection proposal N4113. So, at least for enums declared through this macro, you can have the proposed C++17 enum reflection now, in C++11/C++14.
It is straightforward to extend this answer to the features of the library – nothing "important" is left out here. It is, however, quite tedious, and there are compiler portability concerns.
Disclaimer: I am the author of both the CodeProject article and the library.
You can try the code in this answer, the library, and the implementation of N4428 live online in Wandbox. The library documentation also contains an overview of how to use it as N4428, which explains the enums portion of that proposal.
Explanation
The code below implements conversions between enums and strings. However, it can be extended to do other things as well, such as iteration. This answer wraps an enum in a struct
. You can also generate a traits struct
alongside an enum instead.
The strategy is to generate something like this:
struct Channel {
enum _enum : char { __VA_ARGS__ };
constexpr static const Channel _values[] = { __VA_ARGS__ };
constexpr static const char * const _names[] = { #__VA_ARGS__ };
static const char* _to_string(Channel v) { /* easy */ }
constexpr static Channel _from_string(const char *s) { /* easy */ }
};
The problems are:
- We will end up with something like
{Red = 1, Green, Blue}
as the initializer for the values array. This is not valid C++, because Red
is not an assignable expression. This is solved by casting each constant to a type T
that has an assignment operator, but will drop the assignment: {(T)Red = 1, (T)Green, (T)Blue}
.
- Similarly, we will end up with
{"Red = 1", "Green", "Blue"}
as the initializer for the names array. We will need to trim off the " = 1"
. I am not aware of a great way to do this at compile time, so we will defer this to run time. As a result, _to_string
won't be constexpr
, but _from_string
can still be constexpr
, because we can treat whitespace and equals signs as terminators when comparing with untrimmed strings.
- Both the above need a "mapping" macro that can apply another macro to each element in
__VA_ARGS__
. This is pretty standard. This answer includes a simple version that can handle up to 8 elements.
- If the macro is to be truly self-contained, it needs to declare no static data that requires a separate definition. In practice, this means arrays need special treatment. There are two possible solutions:
constexpr
(or just const
) arrays at namespace scope, or regular arrays in non-constexpr
static inline functions. The code in this answer is for C++11 and takes the former approach. The CodeProject article is for C++98 and takes the latter.
Code
#include <cstddef> // For size_t.
#include <cstring> // For strcspn, strncpy.
#include <stdexcept> // For runtime_error.
// A "typical" mapping macro. MAP(macro, a, b, c, ...) expands to
// macro(a) macro(b) macro(c) ...
// The helper macro COUNT(a, b, c, ...) expands to the number of
// arguments, and IDENTITY(x) is needed to control the order of
// expansion of __VA_ARGS__ on Visual C++ compilers.
#define MAP(macro, ...)
IDENTITY(
APPLY(CHOOSE_MAP_START, COUNT(__VA_ARGS__))
(macro, __VA_ARGS__))
#define CHOOSE_MAP_START(count) MAP ## count
#define APPLY(macro, ...) IDENTITY(macro(__VA_ARGS__))
#define IDENTITY(x) x
#define MAP1(m, x) m(x)
#define MAP2(m, x, ...) m(x) IDENTITY(MAP1(m, __VA_ARGS__))
#define MAP3(m, x, ...) m(x) IDENTITY(MAP2(m, __VA_ARGS__))
#define MAP4(m, x, ...) m(x) IDENTITY(MAP3(m, __VA_ARGS__))
#define MAP5(m, x, ...) m(x) IDENTITY(MAP4(m, __VA_ARGS__))
#define MAP6(m, x, ...) m(x) IDENTITY(MAP5(m, __VA_ARGS__))
#define MAP7(m, x, ...) m(x) IDENTITY(MAP6(m, __VA_ARGS__))
#define MAP8(m, x, ...) m(x) IDENTITY(MAP7(m, __VA_ARGS__))
#define EVALUATE_COUNT(_1, _2, _3, _4, _5, _6, _7, _8, count, ...)
count
#define COUNT(...)
IDENTITY(EVALUATE_COUNT(__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1))
// The type "T" mentioned above that drops assignment operations.
template <typename U>
struct ignore_assign {
constexpr explicit ignore_assign(U value) : _value(value) { }
constexpr operator U() const { return _value; }
constexpr const ignore_assign& operator =(int dummy) const
{ return *this; }
U _value;
};
// Prepends "(ignore_assign<_underlying>)" to each argument.
#define IGNORE_ASSIGN_SINGLE(e) (ignore_assign<_underlying>)e,
#define IGNORE_ASSIGN(...)
IDENTITY(MAP(IGNORE_ASSIGN_SINGLE, __VA_ARGS__))
// Stringizes each argument.
#define STRINGIZE_SINGLE(e) #e,
#define STRINGIZE(...) IDENTITY(MAP(STRINGIZE_SINGLE, __VA_ARGS__))
// Some helpers needed for _from_string.
constexpr const char terminators[] = " =
";
// The size of terminators includes the implicit ''.
constexpr bool is_terminator(char c, size_t index = 0)
{
return
index >= sizeof(terminators) ? false :
c == terminators[index] ? true :
is_terminator(c, index + 1);
}
constexpr bool matches_untrimmed(const char *untrimmed, const char *s,
size_t index = 0)
{
return
is_terminator(untrimmed[index]) ? s[index] == '' :
s[index] != untrimmed[index] ? false :
matches_untrimmed(untrimmed, s, index + 1);
}
// The macro proper.
//
// There are several "simplifications" in this implementation, for the
// sake of brevity. First, we have only one viable option for declaring
// constexpr arrays: at namespace scope. This probably should be done
// two namespaces deep: one namespace that is likely to be unique for
// our little enum "library", then inside it a namespace whose name is
// based on the name of the enum to avoid collisions with other enums.
// I am using only one level of nesting.
//
// Declaring constexpr arrays inside the struct is not viable because
// they will need out-of-line definitions, which will result in
// duplicate symbols when linking. This can be solved with weak
// symbols, but that is compiler- and system-specific. It is not
// possible to declare constexpr arrays as static variables in
// constexpr functions due to the restrictions on such functions.
//
// Note that this prevents the use of this macro anywhere except at
// namespace scope. Ironically, the C++98 version of this, which can
// declare static arrays inside static member functions, is actually
// more flexible in this regard. It is shown in the CodeProject
// article.
//
// Second, for compilation performance reasons, it is best to separate
// the macro into a "parametric" portion, and the portion that depends
// on knowing __VA_ARGS__, and factor the former out into a template.
//
// Third, this code uses a default parameter in _from_string that may
// be better not exposed in the public interface.
#define ENUM(EnumName, Underlying, ...)
namespace data_ ## EnumName {
using _underlying = Underlying;
enum { __VA_ARGS__ };
constexpr const size_t _size =
IDENTITY(COUNT(__VA_ARGS__));
constexpr const _underlying _values[] =
{ IDENTITY(IGNORE_ASSIGN(__VA_ARGS__)) };
constexpr const char * const _raw_names[] =
{ IDENTITY(STRINGIZE(__VA_ARGS__)) };
}
struct EnumName {
using _underlying = Underlying;
enum _enum : _underlying { __VA_ARGS__ };
const char * _to_string() const
{
for (size_t index = 0; index < data_ ## EnumName::_size;
++index) {
if (data_ ## EnumName::_values[index] == _value)
return _trimmed_names()[index];