Search code examples
c++templatesenumscompile-time-constant

Enum convert to string using compile time constants


I'm trying to associate compile time strings to enum values.

Here is my first attempt at the problem:

EnumValue will do the compile time assocation between a string and an enum

template<typename EnumType, int EnumIntValue, const char* EnumStrValue>
class EnumValue
{
public:
    static const char* toString()
    {
        return EnumStrValue;
    }

    static const int toInt()
    {
        return EnumIntValue;
    }

    static EnumType get()
    {
        return static_cast<EnumType>(EnumIntValue);
    }
};

EnumValueHolder will hold the actual values for both string and enum. I dislike my current design as it still needs to hold a pointer to string. I would prefer a compile time association for this but fail to come up with a more elegant solution

template<typename EnumType>
class EnumValueHolder
{
public:
    EnumValueHolder()
    {}

    EnumValueHolder(const EnumType& value, const char* str)
        : value(value), str(str)
    {}

    bool operator==(const EnumValueHolder<EnumType>& rhs) { return value == rhs.value; }
    bool operator==(const EnumType& rhs)const { return value == rhs; }

    operator EnumType()const
    {
        return value;
    }

    const char* toString()const
    {
        return str;
    }

    const int toInt()const
    {
        return static_cast<int>(value);
    }

private:
    EnumType value;
    char const* str;
};

Marcos to easily refer to enum types and enum value holder construction

#define ENUM_VALUE_TYPE(enumName, enumValue) \
EnumValue<enumName, (int)enumName::enumValue, str_##enumValue>


#define ENUM_VALUE_MAKE(enumName, enumValue) \
EnumValueHolder<enumName> { \
    ENUM_VALUE_TYPE(enumName, enumValue)::get(), \
    ENUM_VALUE_TYPE(enumName, enumValue)::toString() }

The following are my test cases and usage examples:

const char str_Apple[] = "Apple";
const char str_Orange[] = "Orange";
const char str_Pineapple[] = "Pineapple";


enum class EFruits
{
    Apple,
    Orange,
    Pineapple
};


int main()
{
    auto evApple = ENUM_VALUE_MAKE(EFruits, Apple);
    std::cout << evApple.toString() << std::endl;

    auto evOrange = ENUM_VALUE_MAKE(EFruits, Orange);
    std::cout << evOrange.toString() << std::endl;

    std::cout << "compare: " << (evApple == evOrange) << std::endl;

    evApple = evOrange;
    std::cout << evApple.toString() << std::endl;

    auto myfruit = ENUM_VALUE_MAKE(EFruits, Pineapple);
    std::cout << myfruit.toString() << std::endl;

    switch (myfruit)
    {
    case EFruits::Apple:
        std::cout << "Im an apple!" << std::endl;
        break;

    case EFruits::Orange:
        std::cout << "Im an Orange!" << std::endl;
        break;

    case EFruits::Pineapple:
        std::cout << "Im a Pineapple!" << std::endl;
        break;

    default:break;
    }
}

One of the objectives is to remove the global string:

const char str_Apple[] = "Apple";
const char str_Orange[] = "Orange";
const char str_Pineapple[] = "Pineapple";

The other is to create a macro that assoicates an enum with a string

//Some crazy define that makes pairs of enum values and strings as
//compile time constants
#define DEFINE_ENUM_STRING(enumValue)\
enumValue, #enumValue

//Ideally, the macro would be used like this. This should be usable in any
//scope (global, namespace, class) 
//with any access specifier (private, protected, public)
enum class EFruits
{
    DEFINE_ENUM_STRING(Apple),
    DEFINE_ENUM_STRING(Orange),
    DEFINE_ENUM_STRING(Pineapple)
};

So there are 2 main questions:

1) Will this current design actually guarantee compile time constants for associating the enum to the string?

2) How can I define a macro to stringify an enum value and declare the value in a enum class using 1 line?

Edit: This should work and compile with msvs2017 on win64 platform using c++ 11.

Thanks.


Solution

  • I think it should work with MSVC2017. It uses C++14 in the constexpr functions but you can split them to single return statement constexprs to be C++11 compatible (however MSVC2017 supports C++14).

    EnumConverter stores the char*, the enum and a string hash value for each enum entry. For each enum you must specialize EnumConverter::StrEnumContainer. The enum-string pairs could be generated with a similar macro you specified.

    #include <tuple>
    #include <array>
    #include <stdexcept>
    
    using namespace std;
    
    enum ELogLevel {
        Info,
        Warn,
        Debug,
        Error,
        Critical
    };
    
    static constexpr size_t constexprStringHash( char const* const str ) noexcept
    {
        return (
            ( *str != 0 ) ?
                ( static_cast< size_t >( *str ) + 33 * constexprStringHash( str + 1 ) ) :
                5381
        );
    }
    
    class EnumConverter final
    {
    public:
    
        EnumConverter() = delete;
        EnumConverter( const EnumConverter& ) = delete;
        EnumConverter( EnumConverter&& ) = delete;
        EnumConverter& operator =( const EnumConverter& ) = delete;
        EnumConverter& operator =( EnumConverter&& ) = delete;
    
        template< typename ENUM_T >
        static constexpr const char* toStr( const ENUM_T value )
        {
            const auto& strEnumArray{ StrEnumContainer< ENUM_T >::StrEnumPairs };
            const char* result{ nullptr };
            for( size_t index{ 0 }; index < strEnumArray.size(); ++index ) {
                if( std::get< 1 >( strEnumArray[ index ] ) == value ) {
                    result = std::get< 0 >( strEnumArray[ index ] );
                    break;
                }
            }
            return ( ( result == nullptr ) ? throw std::logic_error{ "Enum toStrBase conversion failed" } : result );
        }
    
        template< typename ENUM_T >
        static constexpr ENUM_T fromStr( const char* const str )
        {
            const auto& strEnumArray{ StrEnumContainer< ENUM_T >::StrEnumPairs };
            const size_t hash{ constexprStringHash( str ) };
            const ENUM_T* result{ nullptr };
            for( size_t index{ 0 }; index < strEnumArray.size(); ++index ) {
                if( std::get< 2 >( strEnumArray[ index ] ) == hash ) {
                    result = &( std::get< 1 >( strEnumArray[ index ] ) );
                }
            }
            return ( ( result == nullptr ) ? throw std::logic_error{ "Enum toStrBase conversion failed" } : *result );
        }
    
    private:
        template< typename ENUM_T, size_t LEN >
        using ARRAY_T = std::array< std::tuple< const char* const, const ENUM_T, const size_t >, LEN >;
    
        template< typename ENUM_T >
        static constexpr std::tuple< const char* const, ENUM_T, size_t > getTuple( const char* const str, const ENUM_T type ) noexcept
        {
            return std::tuple< const char* const, ENUM_T, size_t >{ str, type, constexprStringHash( str ) };
        }
    
        template< typename ENUM_T >
        struct StrEnumContainer
        {
        };
    
        template< typename ENUM_T >
        friend struct StrEnumContainer;
    };
    
    template<>
    struct EnumConverter::StrEnumContainer< ELogLevel >
    {
        using ENUM_T = ELogLevel;
    
        static constexpr EnumConverter::ARRAY_T< ENUM_T, 5 > StrEnumPairs{ {
            { getTuple( "Info", ENUM_T::Info ) },
            { getTuple( "Warn", ENUM_T::Warn ) },
            { getTuple( "Debug", ENUM_T::Debug ) },
            { getTuple( "Error", ENUM_T::Error ) },
            { getTuple( "Critical", ENUM_T::Critical ) },
        } };
    };
    
    int main()
    {
        //static_assert( EnumConverter::fromStr< ELogLevel >( "Info" ) == EnumConverter::fromStr< ELogLevel >( EnumConverter::toStr( Error ) ), "Error" ); // Error
        static_assert(
            EnumConverter::toStr( Warn )[ 0 ] == 'W' &&
            EnumConverter::toStr( Warn )[ 1 ] == 'a' &&
            EnumConverter::toStr( Warn )[ 2 ] == 'r' &&
            EnumConverter::toStr( Warn )[ 3 ] == 'n',
            "Error"
        );
        static_assert( EnumConverter::fromStr< ELogLevel >( "Info" ) == EnumConverter::fromStr< ELogLevel >( EnumConverter::toStr( Info ) ), "Error" );
    }