Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?
  • 2008 c Easy way to use variables of enum types as string in C?
  • 2008 c++ How to easily map c++ enums to strings
  • 2008 c++ Making something both a C identifier and a string?
  • 2008 c++ Is there a simple script to convert C++ enum to string?
  • 2009 c++ How to use enums as flags in C++?
  • 2011 c++ How to convert an enum type variable to a string?
  • 2011 c++ Enum to String C++
  • 2011 c++ How to convert an enum type variable to a string?
  • 2012 c How to convert enum names to string in c
  • 2013 c Stringifying an conditionally compiled enum in C

After reading many answers, I did not yet find any:

  • Elegant way using C++11, C++14 or C++17 new features
  • Or something ready-to-use in Boost
  • Else something planned for C++20

Example

An example is often better than a long explanation.
You can compile and run this snippet on Coliru.
(Another former example is also available)

#include <map> #include <iostream>  struct MyClass {     enum class MyEnum : char {         AAA = -8,         BBB = '8',         CCC = AAA + BBB     }; };  // Replace magic() by some faster compile-time generated code // (you're allowed to replace the return type with std::string // if that's easier for you) const char* magic (MyClass::MyEnum e) {     const std::map<MyClass::MyEnum,const char*> MyEnumStrings {         { MyClass::MyEnum::AAA, "MyClass::MyEnum::AAA" },         { MyClass::MyEnum::BBB, "MyClass::MyEnum::BBB" },         { MyClass::MyEnum::CCC, "MyClass::MyEnum::CCC" }     };     auto   it  = MyEnumStrings.find(e);     return it == MyEnumStrings.end() ? "Out of range" : it->second; }  int main() {    std::cout << magic(MyClass::MyEnum::AAA) <<'\n';    std::cout << magic(MyClass::MyEnum::BBB) <<'\n';    std::cout << magic(MyClass::MyEnum::CCC) <<'\n'; } 

Constraints

  • Please no valueless duplication of other answers or basic link.
  • Please avoid bloat macro-based answer, or try to reduce the #define overhead as minimum as possible.
  • Please no manual enum -> string mapping.

Nice to have

  • Support enum values starting from a number different from zero
  • Support negative enum values
  • Support fragmented enum values
  • Support class enum (C++11)
  • Support class enum : <type> having any allowed <type> (C++11)
  • Compile-time (not run-time) conversions to a string,
    or at least fast execution at run-time (e.g. std::map is not a great idea...)
  • constexpr (C++11, then relaxed in C++14/17/20)
  • noexcept (C++11)
  • C++17/C++20 friendly snippet

One possible idea could be using the C++ compiler capabilities to generate C++ code at compilation-time using meta-programming tricks based on variadic template class and constexpr functions...

like image 821
oHo Avatar asked Mar 03 '15 10:03

oHo


People also ask

How do I convert an enum to a string?

There are two ways to convert an Enum to String in Java, first by using the name() method of Enum which is an implicit method and available to all Enum, and second by using toString() method.

How do I convert an enum to a string in C ++?

Use const char* Array to Convert an Enum to a String in C++ enum is a built-in type, which can be used to declare small named integers usually formed as an array. This mechanism provides a less error-prone and more readable way of representing a set of integer values.

How do you convert an enum type to a specified string format?

The following example demonstrates converting an enumerated value to a string. type Colors = | Red = 1 | Blue = 2 let myColors = Colors. Red printfn $"The value of this instance is '{myColors. ToString()}'" // Output. // The value of this instance is 'Red'.

Can enum values be string C++?

Here we will see how to map some enum type data to a string in C++. There is no such direct function to do so. But we can create our own function to convert enum to string. We shall create a function that takes an enum value as the argument, and we manually return the enum names as a string from that function.


Video Answer


2 Answers

Magic Enum header-only library provides static reflection for enums (to string, from string, iteration) for C++17.

#include <magic_enum.hpp>  enum Color { RED = 2, BLUE = 4, GREEN = 8 };  Color color = Color::RED; auto color_name = magic_enum::enum_name(color); // color_name -> "RED"  std::string color_name{"GREEN"}; auto color = magic_enum::enum_cast<Color>(color_name) if (color.has_value()) {   // color.value() -> Color::GREEN }; 

For more examples check home repository https://github.com/Neargye/magic_enum.

Where is the drawback?

This library uses a compiler-specific hack (based on __PRETTY_FUNCTION__ / __FUNCSIG__), which works on Clang >= 5, MSVC >= 15.3 and GCC >= 9.

Enum value must be in range [MAGIC_ENUM_RANGE_MIN, MAGIC_ENUM_RANGE_MAX].

  • By default MAGIC_ENUM_RANGE_MIN = -128, MAGIC_ENUM_RANGE_MAX = 128.

  • If need another range for all enum types by default, redefine the macro MAGIC_ENUM_RANGE_MIN and MAGIC_ENUM_RANGE_MAX.

  • MAGIC_ENUM_RANGE_MIN must be less or equals than 0 and must be greater than INT16_MIN.

  • MAGIC_ENUM_RANGE_MAX must be greater than 0 and must be less than INT16_MAX.

  • If need another range for specific enum type, add specialization enum_range for necessary enum type.

    #include <magic_enum.hpp>  enum number { one = 100, two = 200, three = 300 };  namespace magic_enum { template <>   struct enum_range<number> {     static constexpr int min = 100;     static constexpr int max = 300; }; } 
like image 26
Neargye Avatar answered Sep 19 '22 11:09

Neargye


(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:

  1. 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}.
  2. 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.
  3. 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.
  4. 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[] = " =\t\r\n";  // The size of terminators includes the implicit '\0'. 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] == '\0' :         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];                       \         }                                                             \                                                                       \         throw std::runtime_error("invalid value");                    \     }                                                                 \                                                                       \     constexpr static EnumName _from_string(const char *s,             \                                            size_t index = 0)          \     {                                                                 \         return                                                        \             index >= data_ ## EnumName::_size ?                       \                     throw std::runtime_error("invalid identifier") :  \             matches_untrimmed(                                        \                 data_ ## EnumName::_raw_names[index], s) ?            \                     (EnumName)(_enum)data_ ## EnumName::_values[      \                                                             index] :  \             _from_string(s, index + 1);                               \     }                                                                 \                                                                       \     EnumName() = delete;                                              \     constexpr EnumName(_enum value) : _value(value) { }               \     constexpr operator _enum() const { return (_enum)_value; }        \                                                                       \   private:                                                            \     _underlying     _value;                                           \                                                                       \     static const char * const * _trimmed_names()                      \     {                                                                 \         static char     *the_names[data_ ## EnumName::_size];         \         static bool     initialized = false;                          \                                                                       \         if (!initialized) {                                           \             for (size_t index = 0; index < data_ ## EnumName::_size;  \                  ++index) {                                           \                                                                       \                 size_t  length =                                      \                     std::strcspn(data_ ## EnumName::_raw_names[index],\                                  terminators);                        \                                                                       \                 the_names[index] = new char[length + 1];              \                                                                       \                 std::strncpy(the_names[index],                        \                              data_ ## EnumName::_raw_names[index],    \                              length);                                 \                 the_names[index][length] = '\0';                      \             }                                                         \                                                                       \             initialized = true;                                       \         }                                                             \                                                                       \         return the_names;                                             \     }                                                                 \ }; 

and

// The code above was a "header file". This is a program that uses it. #include <iostream> #include "the_file_above.h"  ENUM(Channel, char, Red = 1, Green, Blue)  constexpr Channel   channel = Channel::_from_string("Red");  int main() {     std::cout << channel._to_string() << std::endl;      switch (channel) {         case Channel::Red:   return 0;         case Channel::Green: return 1;         case Channel::Blue:  return 2;     } }  static_assert(sizeof(Channel) == sizeof(char), ""); 

The program above prints Red, as you would expect. There is a degree of type safety, since you can't create an enum without initializing it, and deleting one of the cases from the switch will result in a warning from the compiler (depending on your compiler and flags). Also, note that "Red" was converted to an enum during compilation.

like image 119
antron Avatar answered Sep 20 '22 11:09

antron