3

How to convert an enum to string in C++

 1 year ago
source link: https://mariusbancila.ro/blog/2023/08/17/how-to-convert-an-enum-to-string-in-cpp/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

How to convert an enum to string in C++

Posted on August 17, 2023August 17, 2023 by Marius Bancila

Enumerations are widely used and we often need to convert enum values to strings (typically for recording values in logs) but there is no option at the language level or a utility in the standard library to make it possible. Therefore, developers are usually handcrafting their own solutions.

Let’s say we have the following enum:

enum person
funny,
smart,
intelligent
enum person
{
  funny, 
  smart, 
  intelligent
};

The simplest solution to convert the values to strings is to write a function such as this:

const char* to_string(person p)
switch(p)
case person::funny: return "funny";
case person::smart: return "smart";
case person::intelligent: return "intelligent";
return "<none>"; // or an empty string
// or throw an exception
const char* to_string(person p)
{
  switch(p)
  {
    case person::funny:       return "funny";
    case person::smart:       return "smart";
    case person::intelligent: return "intelligent";
  }
  return "<none>"; // or an empty string
                   // or throw an exception
}

The problems with this solution include the following:

  • You need to explicitly write such a function for every enumeration that you need to be able to convert to string.
  • Every time you modify the enumeration, typically when adding new values, you must modify the serialization function.

It would be helpful to have a sort of automatic way to generate such functions from the definition of an enumeration. A possible approach is using macros.

A solution for this is explained in the article Converting C++ enums to strings by Marcos Cardoso. In consists of several steps:

  • create a set of macros for defining the enum
  • create a set of alternative macro for defining the serialization function
  • use a macro to switch between these two alternative sets of macros, enabling the generation of the serialization function
  • include the header containing the enum definition into a special sourse file that defines the switching macro

In this approach, we could create a person.h header, with the following content:

#if (!defined(PERSON_H) || defined(GENERATE_ENUM_STRINGS))
#if (!defined(GENERATE_ENUM_STRINGS))
#define PERSON_H
#endif
#include "EnumToString.h"
BEGIN_ENUM(person)
DECL_ENUM_ELEMENT(funny)
DECL_ENUM_ELEMENT(smart)
DECL_ENUM_ELEMENT(intelligent)
END_ENUM(person)
#endif PERSON_H
#if (!defined(PERSON_H) || defined(GENERATE_ENUM_STRINGS))

#if (!defined(GENERATE_ENUM_STRINGS))
   #define PERSON_H
#endif

#include "EnumToString.h"

BEGIN_ENUM(person)
{
   DECL_ENUM_ELEMENT(funny)
   DECL_ENUM_ELEMENT(smart)
   DECL_ENUM_ELEMENT(intelligent)
}
END_ENUM(person)

#endif PERSON_H

The source file EnumToString.cpp would contain the following:

#define GENERATE_ENUM_STRINGS // Start string generation
#include "person.h"
#undef GENERATE_ENUM_STRINGS // Stop string generation
#define GENERATE_ENUM_STRINGS  // Start string generation
#include "person.h"             
#undef GENERATE_ENUM_STRINGS   // Stop string generation

The person enum can be used as follows:

#include <iostream>
#include "person.h"
int main()
person p = person::funny;
std::cout << p << '\n';
std::cout << GetStringperson(p) << '\n';
#include <iostream>
#include "person.h"

int main()
{
   person p = person::funny;
   std::cout << p << '\n';
   std::cout << GetStringperson(p) << '\n';
}

The benefit of this solution is that the serialization function is generated automatically; you don’t have to maintain it manually. Whenever a new enumerator is added or an existing one is changed, the changes are reflected immediately in the GetStringX function.

However, this solution has a significant problem: in practice, many enumerations have explicit values defined for the enumerators (or only for some of them). These macros do not support defining such enumerations. But nothing stops us from expanding them to support both. Therefore, I modified the solution to the following:

The header stringified_enum.h contains the following definitions:

#pragma once
#undef DECL_ENUM_ELEMENT
#undef BEGIN_ENUM
#undef END_ENUM
#ifndef GENERATE_ENUM_STRINGS
#define DECLARE_ENUM_ELEMENT(element) element,
#define BEGIN_ENUM(ENUM_NAME, TYPE) typedef enum tag##ENUM_NAME : TYPE {
#define BEGIN_ENUM_INT(ENUM_NAME) BEGIN_ENUM(ENUM_NAME, int)
#define END_ENUM(ENUM_NAME) } ENUM_NAME; const char* ENUM_NAME##_as_string(enum tag##ENUM_NAME index);
#define DECLARE_ENUM_ELEMENT_WITH_VALUE(element, value) element = value,
#define BEGIN_ENUM_WITH_VALUES(ENUM_NAME, TYPE) BEGIN_ENUM(ENUM_NAME, TYPE)
#define BEGIN_ENUM_WITH_VALUES_INT(ENUM_NAME) BEGIN_ENUM(ENUM_NAME, int)
#define END_ENUM_WITH_VALUES(ENUM_NAME) END_ENUM(ENUM_NAME)
#else
#define NO_VALUE "<none>"
#define DECLARE_ENUM_ELEMENT(element) #element,
#define BEGIN_ENUM(ENUM_NAME, TYPE) enum tag##ENUM_NAME : TYPE;\
const char* ENUM_NAME##_as_string(enum tag##ENUM_NAME value) {\
std::size_t index = static_cast<std::size_t>(value);\
static const char* s_##ENUM_NAME[] = {
#define BEGIN_ENUM_INT(ENUM_NAME) BEGIN_ENUM(ENUM_NAME, int)
#define END_ENUM(ENUM_NAME) };\
static const std::size_t s_##ENUM_NAME_len = sizeof(s_##ENUM_NAME)/sizeof(const char*);\
if(index >=0 && index < s_##ENUM_NAME_len)\
return s_##ENUM_NAME[index]; \
return NO_VALUE;\
#define DECLARE_ENUM_ELEMENT_WITH_VALUE(element, value) {value, #element},
#define BEGIN_ENUM_WITH_VALUES(ENUM_NAME, TYPE) enum tag##ENUM_NAME : TYPE;\
const char* ENUM_NAME##_as_string(enum tag##ENUM_NAME value) {\
std::map<TYPE, const char*> sv = {
#define BEGIN_ENUM_WITH_VALUES_INT(ENUM_NAME) BEGIN_ENUM_WITH_VALUES(ENUM_NAME, int)
#define END_ENUM_WITH_VALUES(ENUM_NAME) };\
auto it = sv.find(value);\
if (it != sv.end())\
return it->second;\
return NO_VALUE;\
#endif
#pragma once

#undef DECL_ENUM_ELEMENT
#undef BEGIN_ENUM
#undef END_ENUM

#ifndef GENERATE_ENUM_STRINGS

   #define DECLARE_ENUM_ELEMENT(element)  element,
   #define BEGIN_ENUM(ENUM_NAME, TYPE)    typedef enum tag##ENUM_NAME : TYPE {
   #define BEGIN_ENUM_INT(ENUM_NAME)      BEGIN_ENUM(ENUM_NAME, int)
   #define END_ENUM(ENUM_NAME)            } ENUM_NAME; const char* ENUM_NAME##_as_string(enum tag##ENUM_NAME index);

   #define DECLARE_ENUM_ELEMENT_WITH_VALUE(element, value)  element = value,
   #define BEGIN_ENUM_WITH_VALUES(ENUM_NAME, TYPE)          BEGIN_ENUM(ENUM_NAME, TYPE)
   #define BEGIN_ENUM_WITH_VALUES_INT(ENUM_NAME)            BEGIN_ENUM(ENUM_NAME, int)
   #define END_ENUM_WITH_VALUES(ENUM_NAME)                  END_ENUM(ENUM_NAME)

#else
   #define NO_VALUE                       "<none>"

   #define DECLARE_ENUM_ELEMENT(element)     #element,

   #define BEGIN_ENUM(ENUM_NAME, TYPE)       enum tag##ENUM_NAME : TYPE;\
const char* ENUM_NAME##_as_string(enum tag##ENUM_NAME value) {\
   std::size_t index = static_cast<std::size_t>(value);\
   static const char* s_##ENUM_NAME[] = {

   #define BEGIN_ENUM_INT(ENUM_NAME)         BEGIN_ENUM(ENUM_NAME, int)

   #define END_ENUM(ENUM_NAME)               };\
   static const std::size_t s_##ENUM_NAME_len = sizeof(s_##ENUM_NAME)/sizeof(const char*);\
   if(index >=0 && index < s_##ENUM_NAME_len)\
      return s_##ENUM_NAME[index]; \
   return NO_VALUE;\
}

   #define DECLARE_ENUM_ELEMENT_WITH_VALUE(element, value) {value, #element},

   #define BEGIN_ENUM_WITH_VALUES(ENUM_NAME, TYPE)          enum tag##ENUM_NAME : TYPE;\
const char* ENUM_NAME##_as_string(enum tag##ENUM_NAME value) {\
   std::map<TYPE, const char*> sv = {

   #define BEGIN_ENUM_WITH_VALUES_INT(ENUM_NAME)            BEGIN_ENUM_WITH_VALUES(ENUM_NAME, int)

   #define END_ENUM_WITH_VALUES(ENUM_NAME)                  };\
   auto it = sv.find(value);\
   if (it != sv.end())\
      return it->second;\
   return NO_VALUE;\
}

#endif

In this case, the person.h header containing the person enum definition changes to the following:

#if (!defined(PERSON_H) || defined(GENERATE_ENUM_STRINGS))
#if (!defined(GENERATE_ENUM_STRINGS))
#define PERSON_H
#endif
#include "stringified_enum.h"
BEGIN_ENUM(person, int)
DECLARE_ENUM_ELEMENT(funny)
DECLARE_ENUM_ELEMENT(smart)
DECLARE_ENUM_ELEMENT(intelligent)
END_ENUM(person)
#endif PERSON_H
#if (!defined(PERSON_H) || defined(GENERATE_ENUM_STRINGS))

#if (!defined(GENERATE_ENUM_STRINGS))
   #define PERSON_H
#endif

#include "stringified_enum.h"

BEGIN_ENUM(person, int)
   DECLARE_ENUM_ELEMENT(funny)
   DECLARE_ENUM_ELEMENT(smart)
   DECLARE_ENUM_ELEMENT(intelligent)
END_ENUM(person)

#endif PERSON_H

There is no significant different than the original macros, except for the name and the curly braces that are now part of the begin and end macros. Also, an underlying type, in this case int, is specified explicitly. There is a macro BEGIN_ENUM_INT that implicitly uses int for this purpose.

However, should the enumeration explicitly define values, then the definition of the enum would change to the following:

BEGIN_ENUM_WITH_VALUES(person, int)
DECLARE_ENUM_ELEMENT_WITH_VALUE(funny, 1)
DECLARE_ENUM_ELEMENT_WITH_VALUE(smart, 3)
DECLARE_ENUM_ELEMENT_WITH_VALUE(intelligent, 7)
END_ENUM_WITH_VALUES(person)
BEGIN_ENUM_WITH_VALUES(person, int)
   DECLARE_ENUM_ELEMENT_WITH_VALUE(funny, 1)
   DECLARE_ENUM_ELEMENT_WITH_VALUE(smart, 3)
   DECLARE_ENUM_ELEMENT_WITH_VALUE(intelligent, 7)
END_ENUM_WITH_VALUES(person)

The macros are similar, but they are suffixed with _WITH_VALUE/_WITH_VALUES. DECLARE_ENUM_ELEMENT_WITH_VALUE requires specifying a value for the enumerator.

The source file stringified_enum.cpp must be compiled with the rest of the program and contains the following:

#define GENERATE_ENUM_STRINGS // Start string generation
#include <map>
#include "person.h"
#undef GENERATE_ENUM_STRINGS // Stop string generation
#define GENERATE_ENUM_STRINGS  // Start string generation

#include <map>

#include "person.h"

#undef GENERATE_ENUM_STRINGS   // Stop string generation 

Every time you define a new enumeration using this macros, you must include its header in this source file.

There is a slight change in usage, as the GetStringX serialization function is now called x_as_string:

#include <iostream>
#include "person.h"
int main()
person p = person::funny;
std::cout << p << '\n';
std::cout << person_as_string(p) << '\n';
#include <iostream>
#include "person.h"

int main()
{
   person p = person::funny;
   std::cout << p << '\n';
   std::cout << person_as_string(p) << '\n';
}

But macros are evil!

We have heard this mantra so many times. The good news, for those of you that want something different, is that solutions that don’t require macros also exist. Such a solution is the library called Magic Enum C++.

With this library, you define the enum without any macros, and use magic_enum::enum_name() to get the name of an enumerator:

#include <iostream>
#include "magicenum.h"
enum person
funny,
smart,
intelligent
int main()
person p = person::funny;
std::cout << p << '\n';
std::cout << magic_enum::enum_name(p) << '\n';
#include <iostream>
#include "magicenum.h"

enum person
{
  funny,
  smart,
  intelligent
};

int main()
{
   person p = person::funny;
   std::cout << p << '\n';
   std::cout << magic_enum::enum_name(p) << '\n';
}

The reason I couldn’t use Magic Enum was that it defines some global objects. And then, it was used from the initialization of another global in another translation unit. And since there is no order defined for the initialization of globals in different translation units I could not rely on it.

But global are evil!

Yes. So stop using std::cout.

The macros approach only works for unscoped enums. Magic Enum also works with scoped enums.

Like this:

Loading...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK