23

In order to make my code shorter and easier to change I want to replace something like

enum{ E_AAA, E_BBB, E_CCC };
static const char *strings{"AAA", "BBB", "CCC" };

With a macro, like INIT(AAA, BBB, CCC); but when I try doing a macro with variable arguments, and stringification I get an error as the arguments are not declared.

Any idea on how to do this?

Brian Tompsett - 汤莱恩
  • 5,195
  • 62
  • 50
  • 120
Tiago
  • 231
  • 1
  • 2
  • 3

8 Answers8

31

Here a solution I learned a few days ago. The simplified version that attends your question is:

#define ENUM_MACRO(name, v1, v2, v3, v4, v5, v6, v7)\
    enum name { v1, v2, v3, v4, v5, v6, v7};\
    const char *name##Strings[] = { #v1, #v2, #v3, #v4, #v5, #v6, #v7};

ENUM_MACRO(Week, Sun, Mon, Tue, Wed, Thu, Fri, Sat);

But you can have an improved version, with a function call, like this:

#define ENUM_MACRO(name, v1, v2, v3, v4, v5, v6, v7)\
    enum name { v1, v2, v3, v4, v5, v6, v7};\
    const char *name##Strings[] = { #v1, #v2, #v3, #v4, #v5, #v6, #v7};\
    const char *name##ToString(value) { return name##Strings[value]; }

ENUM_MACRO(Week, Sun, Mon, Tue, Wed, Thu, Fri, Sat);

This will grow to be:

  enum Week { Sun, Mon, Tue, Wed, Thu, Fri, Sat}; 
  const char *WeekStrings[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; 
  const char *WeekToString(value) { return WeekStrings[value]; };

You can even use an offset for the first element, like this one:

#define ENUM_MACRO(name, offset, v1, v2, v3, v4, v5, v6, v7)\
    enum name { v1 =  offset, v2, v3, v4, v5, v6, v7};\
    const char *name##Strings[] = { #v1, #v2, #v3, #v4, #v5, #v6, #v7};\
    const char *name##ToString(value) { return name##Strings[value - offset ]; }

ENUM_MACRO(Week, 1, Sun, Mon, Tue, Wed, Thu, Fri, Sat);

I hope this helps.

Take care, Beco

Reference:

Print the month question, by Kush, answer by Danny Varod

Community
  • 1
  • 1
DrBeco
  • 9,819
  • 7
  • 52
  • 70
  • Please, moderator, remove the CW flag. Thank you. – DrBeco Apr 03 '11 at 17:30
  • As we can see the OP is not active in the site. Is it possible (and desirable!) to a moderator accept my answer as the "accepted answer" (if one think it has value for that, of course)? Thanks for any comment on the procedure. – DrBeco Sep 26 '14 at 02:07
  • It's not possible for anyone but the question author to accept an answer. There's been [discussions about an automatic default](http://meta.stackexchange.com/questions/8692/force-accepted-answers-on-questions-by-inactive-users) for cases like this, but 'accepted' was never intended to indicate 'best', just the answer that the question author could relate to the most. Only the question author knows that for sure, so we've been very reluctant to introduce that sort of change. – Tim Post Sep 26 '14 at 03:34
  • Nice, but the macro is not reusable like the CREATE_ENUM and CREATE_STRINGS macros in one of the other answers. – Paul Floyd Dec 11 '17 at 11:48
  • Thanks Paul (nice name for a band). That was commented in 2011/2014, so the context was different. Anyhow, I still like my answer. Macros for "general things" are never used at full potential. You spend a lot of time to create a general macro only to recreate another "even better" when programming another software. Simplicity is better. Take care. – DrBeco Dec 15 '17 at 01:16
16

You can do it with a bit of macro magic:

#define FRUITS \
    etype(Unknown), \
    etype(Apple),   \
    etype(Orange),  \
    etype(Banana),  \
    etype(Apricot), \
    etype(Mango)

#define etype(x) F_##x

typedef enum { FRUITS } Fruit;

#undef etype
#define etype(x) #x

static const char *strFruit[] = { FRUITS };

Here is a test program:

#include <iostream>
#include <exception>
#include <vector>

#define FRUITS \
    etype(Unknown), \
    etype(Apple),   \
    etype(Orange),  \
    etype(Banana),  \
    etype(Apricot), \
    etype(Mango)

#define etype(x) F_##x

typedef enum { FRUITS } Fruit;

#undef etype
#define etype(x) #x

static const char *strFruit[] = { FRUITS };

const char *enum2str (Fruit f)
{
    return strFruit[static_cast<int>(f)];
}

Fruit str2enum (const char *f)
{
    const int n = sizeof(strFruit) / sizeof(strFruit[0]);
    for (int i = 0; i < n; ++i)
    {
        if (strcmp(strFruit[i], f) == 0)
            return (Fruit) i;
    }
    return F_Unknown;
}

int main (int argc, char *argv[])
{
    std::cout << "I like " << enum2str(F_Mango) << std::endl;
    std::cout << "I do not like " << enum2str(F_Banana) << std::endl;
    std::vector<char *> v;
    v.push_back("Apple");
    v.push_back("Mango");
    v.push_back("Tomato");
    for (int i = 0; i < v.size(); ++i)
    {
        const Fruit f = str2enum(v[i]);
        if (f == F_Unknown)
            std::cout << "Is " << v[i] << " a fruit?" << std::endl;
        else
            std::cout << v[i] << " is a fruit" << std::endl;
    }
    return 0;
}

It outputs:

I like Mango
I do not like Banana
Apple is a fruit
Mango is a fruit
Is Tomato a fruit?
Scott Smedley
  • 1,529
  • 16
  • 27
  • 1
    I like this, but I found a similar but cleaner way here: http://stackoverflow.com/a/238157/599142 – tr3w Jul 13 '12 at 16:14
13

Here is my solution:

#define FRUITS(fruit) \
  fruit(Apple)        \
  fruit(Orange)       \
  fruit(Banana)       

#define CREATE_ENUM(name) \
  F_##name,

#define CREATE_STRINGS(name) \
  #name,

The trick is that 'fruit' is an argument of the macro 'FRUITS' and will be replaced by what ever you pass to. For example:

FRUITS(CREATE_ENUM)

will expand to this:

F_Apple, F_Orange, F_Banana, 

Lets create the enum and the string array:

enum fruit {
  FRUITS(CREATE_ENUM)
};

const char* fruit_names[] = {
  FRUITS(CREATE_STRINGS)
};
indigo
  • 151
  • 1
  • 4
6

One way to do this is with X-Macros, which are basically a way to define a macro which is then used for generating more complex structures than a simple macro easily allows. Here is an example of doing exactly what you are asking.

Chris Pitman
  • 12,448
  • 3
  • 39
  • 56
  • +1 I was just writing this, but you found an already existing example :) – digEmAll Apr 03 '11 at 14:52
  • Bad example for X-Macros, since the procressor can already stringify arguments; meaning that the whole X(red, "red") can be replaced with X(red) with a better X. – UKMonkey Sep 13 '17 at 11:27
6

Here is a solution with Boost.Preprocessor:

#include <boost/preprocessor.hpp>

#define DEFINE_ENUM_DECL_VAL(r, name, val) BOOST_PP_CAT(name, BOOST_PP_CAT(_, val))
#define DEFINE_ENUM_VAL_STR(r, name, val) BOOST_PP_STRINGIZE(val)
#define DEFINE_ENUM(name, val_seq)                                                 \
  enum name {                                                                      \
    BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(DEFINE_ENUM_DECL_VAL, name, val_seq)) \
  };                                                                               \
  static const char* BOOST_PP_CAT(name, _strings[] = ) {                           \
    BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(DEFINE_ENUM_VAL_STR, name, val_seq)) \
  };

DEFINE_ENUM(E, (AAA)(BBB)(CCC))

(AAA)(BBB)(CCC) is a Boost.Preprocessor sequence of tree elements AAA, BBB and CCC; the macro append the enum name to it's modalities:

enum E { E_AAA, E_BBB, E_CCC };
static const char* E_strings[] = { "AAA", "BBB", "CCC" };
Flo
  • 3
  • 3
Begemoth
  • 1,349
  • 9
  • 13
3

I am a bit late to the party but here is another suggestion.
It creates a strongly typed enum class, say MyEnumName and a companion static helper class Enumator<MyEnumName>.
It's bigger than previous answers as it has more features, e.g. stream operators for conversion from/to string.
Note that it relies on c++ 14 standard due to the use of index sequence.

Usage:

/* One line definition - no redundant info */
ENUM_DEFINE(WeekDay /*first item is enum name*/, 
    Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday);

/* works seemlessly with streams (good for logging) */
auto dayOne = WeekDay::Sunday;
std::cout << "day of week is: " << day_of_week;

/* explicit construction from string using WeekDay_enum companion class*/
auto dayTwo = Enumator<WeekDay>::fromString("Tuesday");


/*Iterate over all enum values using Enumator<WeekDay> companion class*/
std::cout << "Days of the week are:\n"
for (auto enumVal : Enumator<WeekDay>::getValues()) {
    std::cout << enumVal << "\n";
}

Source:

    #include <array>
    #include <string>
    #include <sstream>
    #include <stdexcept>

template<typename E>
using isEnum = typename std::enable_if<std::is_enum<E>::value>::type;

template<typename E, typename = isEnum<E>>
constexpr static int enumSize() {
    return 0;
}

template<typename E, typename = isEnum<E>>
inline static std::string getEnumStringValues() {
    return "";
}


/*Enum companion class to hold the methods that can't be declared in an enum*/
template<typename EnumType, isEnum<EnumType>* = nullptr>
class Enumator
{
    Enumator() = delete; /* prevents instantiation */

public:

    constexpr static int size() {
        return enumSize<EnumType>();
    }
    /* list of all enum values a string */
    static auto const& getValuesStr()
    {
        static std::array<std::string, size()> values;
        if (values[0].empty()) {
            std::string valuesStr = getEnumStringValues<EnumType>();
            std::stringstream ss(valuesStr);
            for (auto& value : values) {
                std::getline(ss, value, ',');                   
            }
        }
        return values;
    };

    /* list of all enum values */
    static auto const& getValues()
    {
        static std::array<EnumType, size()> values{ make_array(std::make_index_sequence<size()>()) };
        return values;
    };

    /* To/from string conversion */
    constexpr static std::string const& toString(EnumType arg) {
        return getValuesStr()[static_cast<unsigned>(arg)];
    }

    static EnumType fromString(std::string const& val)
    {
        /* Attempt at converting from string value */
        auto const& strValues = getValuesStr();

        for (unsigned int i = 0; i < strValues.size(); i++)
        { 
            if (val == strValues[i])
            {
                return static_cast<EnumType>(i);
            }
        }
        throw std::runtime_error("No matching enum value found for token: " + val);
    }

private:
    /* Helper method to initialize array of enum values */
    template<std::size_t...Idx>
    static auto make_array(std::index_sequence<Idx...>)
    {
        return std::array<EnumType, size()>{{static_cast<EnumType>(Idx)...}};
    }
};

template<typename EnumType, isEnum<EnumType>* = nullptr>
inline std::istream& operator>> (std::istream& input, EnumType& arg)
{
    std::string val;
    input >> val;
    arg = Enumator<EnumType>::fromString(val);
    return input;
}

template<typename EnumType, isEnum<EnumType>* = nullptr>
inline std::ostream& operator<< (std::ostream& output, const EnumType& arg)
{
    return output << Enumator<EnumType>::toString(arg);
}

#define ENUM_DEFINE(EnumName,...)\
    \
    enum class EnumName;\
    \
    template<>\
    constexpr int enumSize<EnumName>() {\
        /*Trick to get the number of enum members:*/\
        /*dump all the enum values in an array and compute its size */\
        enum EnumName { __VA_ARGS__ }; \
        EnumName enumArray[]{ __VA_ARGS__ }; \
        return sizeof(enumArray) / sizeof(enumArray[0]); \
    }\
    \
    template<>\
    inline std::string getEnumStringValues<EnumName>() { return #__VA_ARGS__; }\
    \
    enum class EnumName : int { __VA_ARGS__ }
gg99
  • 172
  • 1
  • 9
  • This code example is very helpful and was a good start. But please note it has a few shortcomings that may need to be addressed. First, in its current form, the macro can not be used in namespaces or classes. Second, it does not explicitly remove white-spaces from enum strings. This is not strictly required but may be unexpected. Both points are possible to address with some effort, giving a nice and very flexible solution. Thanks @gg99! – emmenlau Apr 29 '20 at 09:00
2

One way to handle this is to define a list macro, i.e. something that expands to another macro that is left for the user to define. For example:

#define MY_LIST MY_ENTRY(AAA) MY_ENTRY(BBB) MY_ENTRY(CCC)

To define the enum:

#define MY_ENTRY(x) E_##x,
enum name
{
  MY_LIST
  NUMBER_OF_ELEMENTS    /* Needed to eat trailing comma (not needed in C99, but in C++) */
};
#undef MY_ENTRY

To define the string:

#define MY_ENTRY(x) #x,
static const char *strings[] = { MY_LIST };
#undef MY_ENTRY

Personally, I find this much easier to work with than the X macro, as this does not rely in include-file magic.

Lindydancer
  • 23,104
  • 4
  • 44
  • 67
  • Trailing commas are totally fine in enums. – Xeo Apr 03 '11 at 16:11
  • I had the check the standards, in C99 they're OK (which was news to me -- thanks). However, they are not accepted by neither C89 nor C++ (1998). – Lindydancer Apr 03 '11 at 16:26
  • You're right, Comeau Online also rejects trailing comma in C++03 strict mode. :) Just ignore my previous comment then. – Xeo Apr 03 '11 at 16:32
  • Looks like you just found a problem with the Comeau compiler, C++03 should accept a trailing comma, according to the draft. – Lindydancer Apr 03 '11 at 17:07
  • @Lindydancer: C++03 should not accept a trailing comma, according to the published standard. – Mike Seymour Apr 03 '11 at 17:40
  • This is an X-Macro, using include files with X-Macros is just an extension allowing `MY_LIST` to be moved to a separate file. – Chris Pitman Apr 03 '11 at 17:40
  • The X-Macro requires that you include a file at the *use point* (were `strings` is defined, in the example above). The *List macros* I presented above does not require this. One advantage is that you can place the definition in its natural location and not in a dedicated header file. – Lindydancer Apr 03 '11 at 18:31
1

For a simple solution, I'd recommend something like X-Macros.

For a more complex solution that adds several other features (like range checking, enhanced type safety, optional associated data, etc.), there's a proposed (but never finalized) Boost.Enum library.

Community
  • 1
  • 1
Josh Kelley
  • 50,042
  • 19
  • 127
  • 215