πŸ“œ ⬆️ ⬇️

Convenient conversion of enumerations (enum) to string in C ++

Enumerations have many uses in development. For example, when creating games, they are used for programming character states or possible directions of movement:

enum State {Idle, Fidget, Walk, Scan, Attack}; enum Direction {North, South, East, West}; 

It is much more convenient when, during debugging, a message like β€œ State: Fidget ” is displayed in the console instead of β€œ State: 1 ”. Also, it is often necessary to serialize enums into JSON, YAML or another format, moreover in the form of string values. Besides the fact that strings are easier to perceive than numbers, their use in the serialization format increases the resistance to changes in the numerical values ​​of enumeration constants. Ideally, "Fidget" should reference the Fidget , even if a new constant is declared, and Fidget has a value other than 1.

Unfortunately, in C ++ there is no possibility to easily convert enumeration values ​​to string and back. Therefore, developers are forced to resort to various tricks that require some support: hard-coded transformations or the use of unsightly restrictive syntax, like X-macros. Someone additionally uses the build tools for automatic conversion. Naturally, this only complicates the development process. After all, enumerations have their own syntax and are stored in their own input files, which does not facilitate the work of the build tools in the Makefile or project files.
')
However, using C ++ means it is much easier to solve the problem of converting enumerations to string.

It is possible to avoid all the mentioned difficulties and generate enums with complete reflection on pure C ++. The ad looks like this:

 BETTER_ENUM(State, int, Idle, Fidget, Walk, Scan, Attack) BETTER_ENUM(Direction, int, North, South, East, West) 

Mode of application:

 State state = State::Fidget; state._to_string(); // "Fidget" std::cout << "state: " << state; //  "state: Fidget" state = State::_from_string("Scan"); // State::Scan (3) //   switch,    . switch (state) { case State::Idle: // ... break; // ... } 

This is done with a few tweaks related to the preprocessor and the template. We will talk a little about them at the end of the article.

In addition to converting to string and back, as well as stream I / O, we can also iterate through the generated enums:

 for (Direction direction : Direction._values()) character.try_moving_in_direction(direction); 

You can generate enums with sparse ranges, and then calculate:

 BETTER_ENUM(Flags, char, Allocated = 1, InUse = 2, Visited = 4, Unreachable = 8) Flags::_size(); // 4 

If you work in C ++ 11, you can even generate code based on enumerations, because all conversions and loops can be performed during compilation using constexpr functions. You can, for example, write a constexpr function that will calculate the maximum value of an enumeration and make it available at compile time. Even if the values ​​of the constants are chosen arbitrarily and are not declared in ascending order.

You can download from Github an example of a macro implementation packaged in a library called Better Enums (Improved Enumerations). It is distributed under the BSD license, so you can do anything with it. In this implementation, there is one header file, so using it is very simple, just add enum.h to the project folder. Try it, maybe it will help you in solving your problems.

How it works


To implement conversions between string and enum values, it is necessary to generate the corresponding mapping. Better Enums does this by creating two arrays during compilation. For example, if you have this declaration:

 BETTER_ENUM(Direction, int, North = 1, South = 2, East = 4, West = 8) 

then the macro will redo it into something like this:

 struct Direction { enum _Enum : int {North = 1, South = 2, East = 4, West = 8}; static const int _values[] = {1, 2, 4, 8}; static const char * const _names[] = {"North", "South", "East", "West"}; int _value; // ...,   ... }; 

And then go to the conversion: find the index of the value or string in _values or _names and return its corresponding value or string to another array.

Array of values


_values generated by referring to the constants of the internal enumeration _Enum . This part of the macro looks like this:

  static const int _values[] = {__VA_ARGS__}; 

It transforms into:

  static const int _values[] = {North = 1, South = 2, East = 4, West = 8}; 

This is almost the correct array declaration. The problem is additional initializers like "= 1". To work with them, Better Enums defines an auxiliary type intended for an assignment operator, but ignores the value itself:

 template <typename T> struct _eat { T _value; template <typename Any> _eat& operator =(Any value) { return *this; } //  . explicit _eat(T value) : _value(value) { } //   T. operator T() const { return _value; } //   T. } 

Now you can include the initializer "= 1" in the assignment expression, which has no value:

  static const int _values[] = {(_eat<_Enum>)North = 1, (_eat<_Enum>)South = 2, (_eat<_Enum>)East = 4, (_eat<_Enum>)West = 8}; 

String array


To create this array, Better Enums uses ( # ) - the preprocessor operator for string translation. It converts __VA_ARGS__ into something like this:

  static const char * const _names[] = {"North = 1", "South = 2", "East = 4", "West = 8"}; 

Now we almost convert constant names to string. It remains to get rid of initializers. However, Better Enums does not. Just when comparing _names in the _names array _names it perceives the characters of spaces and equality as additional line boundaries. So when searching for β€œ North = 1 ”, Better Enums will only find β€œ North ”.

Is it possible to do without a macro?


Hardly. The fact is that in C ++ the operator (#) is the only way to convert a source code to a string token. So in any library that automatically converts enums with reflection, you have to use at least one high-level macro.

Other considerations


Of course, it would be much more boring and more difficult to fully consider the implementation of a macro than it is done in this article. Basically, difficulties arise because of the support of constexpr functions working with static constexpr , due to the constexpr of different compilers. Also, certain difficulties may be associated with the decomposition of as much of the macro as possible into templates in order to speed up compilation (templates do not need to be reparsed during creation, but macro extensions are needed).

Source: https://habr.com/ru/post/277757/


All Articles