enum to string in modern C++11 / C++14 / C++17 and future C++20

后端 未结 28 1935
逝去的感伤
逝去的感伤 2020-11-22 16:57

Contrary to all other similar questions, this question is about using the new C++ features.

  • 2008 c Is there a simple way to convert C++ enum to string?
  • <
相关标签:
28条回答
  • 2020-11-22 17:26

    I had the same problem a couple of days ago. I couldn't find any C++ solution without some weird macro magic, so I decided to write a CMake code generator to generate simple switch case statements.

    Usage:

    enum2str_generate(
      PATH          <path to place the files in>
      CLASS_NAME    <name of the class (also prefix for the files)>
      FUNC_NAME     <name of the (static) member function>
      NAMESPACE     <the class will be inside this namespace>
      INCLUDES      <LIST of files where the enums are defined>
      ENUMS         <LIST of enums to process>
      BLACKLIST     <LIST of constants to ignore>
      USE_CONSTEXPR <whether to use constexpr or not (default: off)>
      USE_C_STRINGS <whether to use c strings instead of std::string or not (default: off)>
    )
    

    The function searches the include files in the filesystem (uses the include directories provided with the include_directories command), reads them and does some regex to generate the class and the function(s).

    NOTE: constexpr implies inline in C++, so using the USE_CONSTEXPR option will generate a header only class!

    Example:

    ./includes/a.h:

    enum AAA : char { A1, A2 };
    
    typedef enum {
       VAL1          = 0,
       VAL2          = 1,
       VAL3          = 2,
       VAL_FIRST     = VAL1,    // Ignored
       VAL_LAST      = VAL3,    // Ignored
       VAL_DUPLICATE = 1,       // Ignored
       VAL_STRANGE   = VAL2 + 1 // Must be blacklisted
    } BBB;
    

    ./CMakeLists.txt:

    include_directories( ${PROJECT_SOURCE_DIR}/includes ...)
    
    enum2str_generate(
       PATH       "${PROJECT_SOURCE_DIR}"
       CLASS_NAME "enum2Str"
       NAMESPACE  "abc"
       FUNC_NAME  "toStr"
       INCLUDES   "a.h" # WITHOUT directory
       ENUMS      "AAA" "BBB"
       BLACKLIST  "VAL_STRANGE")
    

    Generates:

    ./enum2Str.hpp:

    /*!
      * \file enum2Str.hpp
      * \warning This is an automatically generated file!
      */
    
    #ifndef ENUM2STR_HPP
    #define ENUM2STR_HPP
    
    #include <string>
    #include <a.h>
    
    namespace abc {
    
    class enum2Str {
     public:
       static std::string toStr( AAA _var ) noexcept;
       static std::string toStr( BBB _var ) noexcept;
    };
    
    }
    
    #endif // ENUM2STR_HPP
    

    ./enum2Str.cpp:

    /*!
      * \file enum2Str.cpp
      * \warning This is an automatically generated file!
      */
    
    #include "enum2Str.hpp"
    
    namespace abc {
    
    /*!
     * \brief Converts the enum AAA to a std::string
     * \param _var The enum value to convert
     * \returns _var converted to a std::string
     */
    std::string enum2Str::toStr( AAA _var ) noexcept {
       switch ( _var ) {
          case A1: return "A1";
          case A2: return "A2";
          default: return "<UNKNOWN>";
       }
    }
    
    /*!
     * \brief Converts the enum BBB to a std::string
     * \param _var The enum value to convert
     * \returns _var converted to a std::string
     */
    std::string enum2Str::toStr( BBB _var ) noexcept {
       switch ( _var ) {
          case VAL1: return "VAL1";
          case VAL2: return "VAL2";
          case VAL3: return "VAL3";
          default: return "<UNKNOWN>";
       }
    }
    }
    

    Update:

    The script now also supports scoped enumerations (enum class|struct) and I moved it to a seperate repo with some other scripts I often use: https://github.com/mensinda/cmakeBuildTools

    0 讨论(0)
  • 2020-11-22 17:26

    my solution is without macro usage.

    advantages:

    • you see exactly what you do
    • access is with hash maps, so good for many valued enums
    • no need to consider order or non-consecutive values
    • both enum to string and string to enum translation, while added enum value must be added in one additional place only

    disadvantages:

    • you need to replicate all the enums values as text
    • access in hash map must consider string case
    • maintenance if adding values is painful - must add in both enum and direct translate map

    so... until the day that C++ implements the C# Enum.Parse functionality, I will be stuck with this:

                #include <unordered_map>
    
                enum class Language
                { unknown, 
                    Chinese, 
                    English, 
                    French, 
                    German
                    // etc etc
                };
    
                class Enumerations
                {
                public:
                    static void fnInit(void);
    
                    static std::unordered_map <std::wstring, Language> m_Language;
                    static std::unordered_map <Language, std::wstring> m_invLanguage;
    
                private:
                    static void fnClear();
                    static void fnSetValues(void);
                    static void fnInvertValues(void);
    
                    static bool m_init_done;
                };
    
                std::unordered_map <std::wstring, Language> Enumerations::m_Language = std::unordered_map <std::wstring, Language>();
                std::unordered_map <Language, std::wstring> Enumerations::m_invLanguage = std::unordered_map <Language, std::wstring>();
    
                void Enumerations::fnInit()
                {
                    fnClear();
                    fnSetValues();
                    fnInvertValues();
                }
    
                void Enumerations::fnClear()
                {
                    m_Language.clear();
                    m_invLanguage.clear();
                }
    
                void Enumerations::fnSetValues(void)
                {   
                    m_Language[L"unknown"] = Language::unknown;
                    m_Language[L"Chinese"] = Language::Chinese;
                    m_Language[L"English"] = Language::English;
                    m_Language[L"French"] = Language::French;
                    m_Language[L"German"] = Language::German;
                    // and more etc etc
                }
    
                void Enumerations::fnInvertValues(void)
                {
                    for (auto it = m_Language.begin(); it != m_Language.end(); it++)
                    {
                        m_invLanguage[it->second] = it->first;
                    }
                }
    
                // usage -
                //Language aLanguage = Language::English;
                //wstring sLanguage = Enumerations::m_invLanguage[aLanguage];
    
                //wstring sLanguage = L"French" ;
                //Language aLanguage = Enumerations::m_Language[sLanguage];
    
    0 讨论(0)
  • 2020-11-22 17:27

    Back in 2011 I spent a weekend fine-tuning a macro-based solution and ended up never using it.

    My current procedure is to start Vim, copy the enumerators in an empty switch body, start a new macro, transform the first enumerator into a case statement, move the cursor to the beginning of the next line, stop the macro and generate the remaining case statements by running the macro on the other enumerators.

    Vim macros are more fun than C++ macros.

    Real-life example:

    enum class EtherType : uint16_t
    {
        ARP   = 0x0806,
        IPv4  = 0x0800,
        VLAN  = 0x8100,
        IPv6  = 0x86DD
    };
    

    I will create this:

    std::ostream& operator<< (std::ostream& os, EtherType ethertype)
    {
        switch (ethertype)
        {
            case EtherType::ARP : return os << "ARP" ;
            case EtherType::IPv4: return os << "IPv4";
            case EtherType::VLAN: return os << "VLAN";
            case EtherType::IPv6: return os << "IPv6";
            // omit default case to trigger compiler warning for missing cases
        };
        return os << static_cast<std::uint16_t>(ethertype);
    }
    

    And that's how I get by.

    Native support for enum stringification would be much better though. I'm very interested to see the results of the reflection workgroup in C++17.

    An alternative way to do it was posted by @sehe in the comments.

    0 讨论(0)
  • 2020-11-22 17:27

    I took the idea from @antron and implemented it differently: generating a true enum class.

    This implementation meets all the requirements listed in original question but currently has only one real limitation: it assumes the enum values are either not provided or, if provided, must start with 0 and go up sequentially without gaps.

    This is not an intrinsic limitation - simply that I don't use ad-hoc enum values. If this is needed, one can replace vector lookup with traditional switch/case implementation.

    The solution uses some c++17 for inline variables but this can be easily avoided if needed. It also uses boost:trim because of simplicity.

    Most importantly, it takes only 30 lines of code and no black magic macros. The code is below. It's meant to be put in header and included in multiple compilation modules.

    It can be used the same way as was suggested earlier in this thread:

    ENUM(Channel, int, Red, Green = 1, Blue)
    std::out << "My name is " << Channel::Green;
    //prints My name is Green
    

    Pls let me know if this is useful and how it can be improved further.


    #include <boost/algorithm/string.hpp>   
    struct EnumSupportBase {
      static std::vector<std::string> split(const std::string s, char delim) {
        std::stringstream ss(s);
        std::string item;
        std::vector<std::string> tokens;
        while (std::getline(ss, item, delim)) {
            auto pos = item.find_first_of ('=');
            if (pos != std::string::npos)
                item.erase (pos);
            boost::trim (item);
            tokens.push_back(item);
        }
        return tokens;
      }
    };
    #define ENUM(EnumName, Underlying, ...) \
        enum class EnumName : Underlying { __VA_ARGS__, _count }; \
        struct EnumName ## Support : EnumSupportBase { \
            static inline std::vector<std::string> _token_names = split(#__VA_ARGS__, ','); \
            static constexpr const char* get_name(EnumName enum_value) { \
                int index = (int)enum_value; \
                if (index >= (int)EnumName::_count || index < 0) \
                   return "???"; \
                else \
                   return _token_names[index].c_str(); \
            } \
        }; \
        inline std::ostream& operator<<(std::ostream& os, const EnumName & es) { \
            return os << EnumName##Support::get_name(es); \
        } 
    
    0 讨论(0)
  • 2020-11-22 17:27

    Solutions using enum within class/struct (struct defaults with public members) and overloaded operators:

    struct Color
    {
        enum Enum { RED, GREEN, BLUE };
        Enum e;
    
        Color() {}
        Color(Enum e) : e(e) {}
    
        Color operator=(Enum o) { e = o; return *this; }
        Color operator=(Color o) { e = o.e; return *this; }
        bool operator==(Enum o) { return e == o; }
        bool operator==(Color o) { return e == o.e; }
        operator Enum() const { return e; }
    
        std::string toString() const
        {
            switch (e)
            {
            case Color::RED:
                return "red";
            case Color::GREEN:
                return "green";
            case Color::BLUE:
                return "blue";
            default:
                return "unknown";
            }
        }
    };
    

    From the outside it looks nearly exactly like a class enum:

    Color red;
    red = Color::RED;
    Color blue = Color::BLUE;
    
    cout << red.toString() << " " << Color::GREEN << " " << blue << endl;
    

    This will output "red 1 2". You could possibly overload << to make blue output a string (although it might cause ambiguity so not possible), but it wouldn't work with Color::GREEN since it doesn't automatically convert to Color.

    The purpose of having an implicit convert to Enum (which implicitly converts to int or type given) is to be able to do:

    Color color;
    switch (color) ...
    

    This works, but it also means that this work too:

    int i = color;
    

    With an enum class it wouldn't compile. You ought to be careful if you overload two functions taking the enum and an integer, or remove the implicit conversion...

    Another solution would involve using an actual enum class and static members:

    struct Color
    {
        enum class Enum { RED, GREEN, BLUE };
        static const Enum RED = Enum::RED, GREEN = Enum::GREEN, BLUE = Enum::BLUE;
    
        //same as previous...
    };
    

    It possibly takes more space, and is longer to make, but causes a compile error for implicit int conversions. I'd use this one because of that!

    There's surely overhead with this though, but I think it's just simpler and looks better than other code I've seen. There's also potential for adding functionality, which could all be scoped within the class.

    Edit: this works and most can be compiled before execution:

    class Color
    {
    public:
        enum class Enum { RED, GREEN, BLUE };
        static const Enum RED = Enum::RED, GREEN = Enum::GREEN, BLUE = Enum::BLUE;
    
        constexpr Color() : e(Enum::RED) {}
        constexpr Color(Enum e) : e(e) {}
    
        constexpr bool operator==(Enum o) const { return e == o; }
        constexpr bool operator==(Color o) const { return e == o.e; }
        constexpr operator Enum() const { return e; }
    
        Color& operator=(Enum o) { const_cast<Enum>(this->e) = o; return *this; }
        Color& operator=(Color o) { const_cast<Enum>(this->e) = o.e; return *this; }
    
        std::string toString() const
        {
            switch (e)
            {
            case Enum::RED:
                return "red";
            case Enum::GREEN:
                return "green";
            case Enum::BLUE:
                return "blue";
            default:
                return "unknown";
            }
        }
    private:
        const Enum e;
    };
    
    0 讨论(0)
  • 2020-11-22 17:27

    My answer is here.

    You can get enum value names and these indices simultaneously as deque of string.

    This method only needs little copy and paste and edit.

    Obtained result needs type-casting from size_t to enum class type when you need enum class type value, but I think it is a very portable and powerful way to treat enum class.

    enum class myenum
    {
      one = 0,
      two,
      three,
    };
    
    deque<string> ssplit(const string &_src, boost::regex &_re)
    {
      boost::sregex_token_iterator it(_src.begin(), _src.end(), _re, -1);
      boost::sregex_token_iterator e;
      deque<string> tokens;
      while (it != e)
        tokens.push_back(*it++);
      return std::move(tokens);
    }
    
    int main()
    {
      regex re(",");
      deque<string> tokens = ssplit("one,two,three", re);
      for (auto &t : tokens) cout << t << endl;
        getchar();
      return 0;
    }
    
    0 讨论(0)
提交回复
热议问题