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

后端 未结 28 1933
逝去的感伤
逝去的感伤 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:34

    As long as you are okay with writing a separate .h/.cpp pair for each queryable enum, this solution works with nearly the same syntax and capabilities as a regular c++ enum:

    // MyEnum.h
    #include <EnumTraits.h>
    #ifndef ENUM_INCLUDE_MULTI
    #pragma once
    #end if
    
    enum MyEnum : int ETRAITS
    {
        EDECL(AAA) = -8,
        EDECL(BBB) = '8',
        EDECL(CCC) = AAA + BBB
    };
    

    The .cpp file is 3 lines of boilerplate:

    // MyEnum.cpp
    #define ENUM_DEFINE MyEnum
    #define ENUM_INCLUDE <MyEnum.h>
    #include <EnumTraits.inl>
    

    Example usage:

    for (MyEnum value : EnumTraits<MyEnum>::GetValues())
        std::cout << EnumTraits<MyEnum>::GetName(value) << std::endl;
    

    Code

    This solution requires 2 source files:

    // EnumTraits.h
    #pragma once
    #include <string>
    #include <unordered_map>
    #include <vector>
    
    #define ETRAITS
    #define EDECL(x) x
    
    template <class ENUM>
    class EnumTraits
    {
    public:
        static const std::vector<ENUM>& GetValues()
        {
            return values;
        }
    
        static ENUM GetValue(const char* name)
        {
            auto match = valueMap.find(name);
            return (match == valueMap.end() ? ENUM() : match->second);
        }
    
        static const char* GetName(ENUM value)
        {
            auto match = nameMap.find(value);
            return (match == nameMap.end() ? nullptr : match->second);
        }
    
    public:
        EnumTraits() = delete;
    
        using vector_type = std::vector<ENUM>;
        using name_map_type = std::unordered_map<ENUM, const char*>;
        using value_map_type = std::unordered_map<std::string, ENUM>;
    
    private:
        static const vector_type values;
        static const name_map_type nameMap;
        static const value_map_type valueMap;
    };
    
    struct EnumInitGuard{ constexpr const EnumInitGuard& operator=(int) const { return *this; } };
    template <class T> constexpr T& operator<<=(T&& x, const EnumInitGuard&) { return x; }
    

    ...and

    // EnumTraits.inl
    #define ENUM_INCLUDE_MULTI
    
    #include ENUM_INCLUDE
    #undef ETRAITS
    #undef EDECL
    
    using EnumType = ENUM_DEFINE;
    using TraitsType = EnumTraits<EnumType>;
    using VectorType = typename TraitsType::vector_type;
    using NameMapType = typename TraitsType::name_map_type;
    using ValueMapType = typename TraitsType::value_map_type;
    using NamePairType = typename NameMapType::value_type;
    using ValuePairType = typename ValueMapType::value_type;
    
    #define ETRAITS ; const VectorType TraitsType::values
    #define EDECL(x) EnumType::x <<= EnumInitGuard()
    #include ENUM_INCLUDE
    #undef ETRAITS
    #undef EDECL
    
    #define ETRAITS ; const NameMapType TraitsType::nameMap
    #define EDECL(x) NamePairType(EnumType::x, #x) <<= EnumInitGuard()
    #include ENUM_INCLUDE
    #undef ETRAITS
    #undef EDECL
    
    #define ETRAITS ; const ValueMapType TraitsType::valueMap
    #define EDECL(x) ValuePairType(#x, EnumType::x) <<= EnumInitGuard()
    #include ENUM_INCLUDE
    #undef ETRAITS
    #undef EDECL
    

    Explanation

    This implementation exploits the fact that the braced list of elements of an enum definition can also be used as a braced initializer list for class member initialization.

    When ETRAITS is evaluated in the context of EnumTraits.inl, it expands out to a static member definition for the EnumTraits<> class.

    The EDECL macro transforms each enum member into initializer list values which subsequently get passed into the member constructor in order to populate the enum info.

    The EnumInitGuard class is designed to consume the enum initializer values and then collapse - leaving a pure list of enum data.

    Benefits

    • c++-like syntax
    • Works identically for both enum and enum class (*almost)
    • Works for enum types with any numeric underlying type
    • Works for enum types with automatic, explicit, and fragmented initializer values
    • Works for mass renaming (intellisense linking preserved)
    • Only 5 preprocessor symbols (3 global)

    * In contrast to enums, initializers in enum class types that reference other values from the same enum must have those values fully qualified

    Disbenefits

    • Requires a separate .h/.cpp pair for each queryable enum
    • Depends on convoluted macro and include magic
    • Minor syntax errors explode into much larger errors
    • Defining class or namespace scoped enums is nontrivial
    • No compile time initialization

    Comments

    Intellisense will complain a bit about private member access when opening up EnumTraits.inl, but since the expanded macros are actually defining class members, that isn't actually a problem.

    The #ifndef ENUM_INCLUDE_MULTI block at the top of the header file is a minor annoyance that could probably be shrunken down into a macro or something, but it's small enough to live with at its current size.

    Declaring a namespace scoped enum requires that the enum first be forward declared inside its namespace scope, then defined in the global namespace. Additionally, any enum initializers using values of the same enum must have those values fully qualified.

    namespace ns { enum MyEnum : int; }
    enum ns::MyEnum : int ETRAITS
    {
        EDECL(AAA) = -8,
        EDECL(BBB) = '8',
        EDECL(CCC) = ns::MyEnum::AAA + ns::MyEnum::BBB
    }
    
    0 讨论(0)
  • 2020-11-22 17:34

    I wrote a library for solving this problem, everything happens in compiling time, except for getting the message.

    Usage:

    Use macro DEF_MSG to define a macro and message pair:

    DEF_MSG(CODE_OK,   "OK!")
    DEF_MSG(CODE_FAIL, "Fail!")
    

    CODE_OK is the macro to use, and "OK!" is the corresponding message.

    Use get_message() or just gm() to get the message:

    get_message(CODE_FAIL);  // will return "Fail!"
    gm(CODE_FAIL);           // works exactly the same as above
    

    Use MSG_NUM to find out how many macros have been defined. This will automatically increse, you don't need to do anything.

    Predefined messages:

    MSG_OK:     OK
    MSG_BOTTOM: Message bottom
    

    Project: libcodemsg


    The library doesn't create extra data. Everything happens in compiling time. In message_def.h, it generates an enum called MSG_CODE; in message_def.c, it generates a variable holds all the strings in static const char* _g_messages[].

    In such case, the library is limited to create one enum only. This is ideal for return values, for example:

    MSG_CODE foo(void) {
        return MSG_OK; // or something else
    }
    
    MSG_CODE ret = foo();
    
    if (MSG_OK != ret) {
        printf("%s\n", gm(ret););
    }
    

    Another thing I like this design is you can manage message definitions in different files.


    I found the solution to this question looks much better.

    0 讨论(0)
  • 2020-11-22 17:35
    #define ENUM_MAKE(TYPE, ...) \
            enum class TYPE {__VA_ARGS__};\
            struct Helper_ ## TYPE { \
                static const String& toName(TYPE type) {\
                    int index = static_cast<int>(type);\
                    return splitStringVec()[index];}\
                static const TYPE toType(const String& name){\
                    static std::unordered_map<String,TYPE> typeNameMap;\
                    if( typeNameMap.empty() )\
                    {\
                        const StringVector& ssVec = splitStringVec();\
                        for (size_t i = 0; i < ssVec.size(); ++i)\
                            typeNameMap.insert(std::make_pair(ssVec[i], static_cast<TYPE>(i)));\
                    }\
                    return typeNameMap[name];}\
                static const StringVector& splitStringVec() {\
                    static StringVector typeNameVector;\
                    if(typeNameVector.empty()) \
                    {\
                        typeNameVector = StringUtil::split(#__VA_ARGS__, ",");\
                        for (auto& name : typeNameVector)\
                        {\
                            name.erase(std::remove(name.begin(), name.end(), ' '),name.end()); \
                            name = String(#TYPE) + "::" + name;\
                        }\
                    }\
                    return typeNameVector;\
                }\
            };
    
    
    using String = std::string;
    using StringVector = std::vector<String>;
    
       StringVector StringUtil::split( const String& str, const String& delims, unsigned int maxSplits, bool preserveDelims)
        {
            StringVector ret;
            // Pre-allocate some space for performance
            ret.reserve(maxSplits ? maxSplits+1 : 10);    // 10 is guessed capacity for most case
    
            unsigned int numSplits = 0;
    
            // Use STL methods 
            size_t start, pos;
            start = 0;
            do 
            {
                pos = str.find_first_of(delims, start);
                if (pos == start)
                {
                    // Do nothing
                    start = pos + 1;
                }
                else if (pos == String::npos || (maxSplits && numSplits == maxSplits))
                {
                    // Copy the rest of the string
                    ret.push_back( str.substr(start) );
                    break;
                }
                else
                {
                    // Copy up to delimiter
                    ret.push_back( str.substr(start, pos - start) );
    
                    if(preserveDelims)
                    {
                        // Sometimes there could be more than one delimiter in a row.
                        // Loop until we don't find any more delims
                        size_t delimStart = pos, delimPos;
                        delimPos = str.find_first_not_of(delims, delimStart);
                        if (delimPos == String::npos)
                        {
                            // Copy the rest of the string
                            ret.push_back( str.substr(delimStart) );
                        }
                        else
                        {
                            ret.push_back( str.substr(delimStart, delimPos - delimStart) );
                        }
                    }
    
                    start = pos + 1;
                }
                // parse up to next real data
                start = str.find_first_not_of(delims, start);
                ++numSplits;
    
            } while (pos != String::npos);
    
    
    
            return ret;
        }
    

    example

    ENUM_MAKE(MY_TEST, MY_1, MY_2, MY_3)
    
    
        MY_TEST s1 = MY_TEST::MY_1;
        MY_TEST s2 = MY_TEST::MY_2;
        MY_TEST s3 = MY_TEST::MY_3;
    
        String z1 = Helper_MY_TEST::toName(s1);
        String z2 = Helper_MY_TEST::toName(s2);
        String z3 = Helper_MY_TEST::toName(s3);
    
        MY_TEST q1 = Helper_MY_TEST::toType(z1);
        MY_TEST q2 = Helper_MY_TEST::toType(z2);
        MY_TEST q3 = Helper_MY_TEST::toType(z3);
    

    automatically ENUM_MAKE macro generate 'enum class' and helper class with 'enum reflection function'.

    In order to reduce mistakes, at once Everything is defined with only one ENUM_MAKE.

    The advantage of this code is automatically created for reflection and a close look at macro code ,easy-to-understand code. 'enum to string' , 'string to enum' performance both is algorithm O(1).

    Disadvantages is when first use , helper class for enum relection 's string vector and map is initialized. but If you want you'll also be pre-initialized. –

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

    I have been frustrated by this problem for a long time too, along with the problem of getting a type converted to string in a proper way. However, for the last problem, I was surprised by the solution explained in Is it possible to print a variable's type in standard C++?, using the idea from Can I obtain C++ type names in a constexpr way?. Using this technique, an analogous function can be constructed for getting an enum value as string:

    #include <iostream>
    using namespace std;
    
    class static_string
    {
        const char* const p_;
        const std::size_t sz_;
    
    public:
        typedef const char* const_iterator;
    
        template <std::size_t N>
        constexpr static_string(const char(&a)[N]) noexcept
            : p_(a)
            , sz_(N - 1)
        {}
    
        constexpr static_string(const char* p, std::size_t N) noexcept
            : p_(p)
            , sz_(N)
        {}
    
        constexpr const char* data() const noexcept { return p_; }
        constexpr std::size_t size() const noexcept { return sz_; }
    
        constexpr const_iterator begin() const noexcept { return p_; }
        constexpr const_iterator end()   const noexcept { return p_ + sz_; }
    
        constexpr char operator[](std::size_t n) const
        {
            return n < sz_ ? p_[n] : throw std::out_of_range("static_string");
        }
    };
    
    inline std::ostream& operator<<(std::ostream& os, static_string const& s)
    {
        return os.write(s.data(), s.size());
    }
    
    /// \brief Get the name of a type
    template <class T>
    static_string typeName()
    {
    #ifdef __clang__
        static_string p = __PRETTY_FUNCTION__;
        return static_string(p.data() + 30, p.size() - 30 - 1);
    #elif defined(_MSC_VER)
        static_string p = __FUNCSIG__;
        return static_string(p.data() + 37, p.size() - 37 - 7);
    #endif
    
    }
    
    namespace details
    {
        template <class Enum>
        struct EnumWrapper
        {
            template < Enum enu >
            static static_string name()
            {
    #ifdef __clang__
                static_string p = __PRETTY_FUNCTION__;
                static_string enumType = typeName<Enum>();
                return static_string(p.data() + 73 + enumType.size(), p.size() - 73 - enumType.size() - 1);
    #elif defined(_MSC_VER)
                static_string p = __FUNCSIG__;
                static_string enumType = typeName<Enum>();
                return static_string(p.data() + 57 + enumType.size(), p.size() - 57 - enumType.size() - 7);
    #endif
            }
        };
    }
    
    /// \brief Get the name of an enum value
    template <typename Enum, Enum enu>
    static_string enumName()
    {
        return details::EnumWrapper<Enum>::template name<enu>();
    }
    
    enum class Color
    {
        Blue = 0,
        Yellow = 1
    };
    
    
    int main() 
    {
        std::cout << "_" << typeName<Color>() << "_"  << std::endl;
        std::cout << "_" << enumName<Color, Color::Blue>() << "_"  << std::endl;
        return 0;
    }
    

    The code above has only been tested on Clang (see https://ideone.com/je5Quv) and VS2015, but should be adaptable to other compilers by fiddling a bit with the integer constants. Of course, it still uses macros under the hood, but at least one doesn't need access to the enum implementation.

    0 讨论(0)
提交回复
热议问题