头图
Prologue: Application of enumerated types in C++ and enhancement of conversion to string: AWESOME_MAKE_ENUM,...
Original From: HERE

Because it was temporarily discovered that a converter from enumeration to string was needed, I simply sorted out the changes in enumeration types throughout the ages.

So strange cold knowledge increased again.

Enumerated type

enum

Before cxx11, C/C++ declared enumerators through the enum keyword.

// 匿名全局枚举量
enum {
  DOG,
  CAT = 100,
  HORSE = 1000
};

enum Animal {
  DOG,
  CAT = 100,
  HORSE = 1000
};

Starting from cxx11, enum allows the use of other data types other than integer. At this time its syntax is like this:

enum 名字(可选) : 类型 { 枚举项 = 常量表达式 , 枚举项 = 常量表达式 , ... }
enum 名字 : 类型 ;

So when necessary:

enum smallenum: std::int16_t {
    a,
    b,
    c
};

However, the type is not allowed to be larger than int, and is subject to the CPU word length. So type support has almost no practical value. I don't know what those people think, it seems embedded is really popular.

enum class

Since cxx11, we are recommended to abandon enum and use scoped enum class or enum struct instead. At this time, the way to declare the enumeration type is as follows:

enum class Animal {
  DOG,
  CAT = 100,
  HORSE = 1000
};

It also supports : type definition, and again, it's useless.

// altitude 可以是 altitude::high 或 altitude::low
enum class altitude: char
{ 
     high='h',
     low='l', // C++11 允许尾随逗号
}; 

the difference

The difference between enum class and enum is restricted scope and strongly typed.

The scope limitation is mainly reflected in the enum class in the class/struct is limited by its periphery. The following example can briefly illustrate:

enum class fruit { orange, apple };
struct S {
  using enum fruit; // OK :引入 orange 与 apple 到 S 中
};
void f() {
    S s;
    s.orange;  // OK :指名 fruit::orange
    S::orange; // OK :指名 fruit::orange
}

This has good support for the situation of the same name inside and outside.

Strong type qualification now rejects implicit coercion and exchange between enumeration and int, but supports static_cast<int>(enum-value) to obtain the underlying int value of enumeration. In addition, when the following conditions are met enumeration can use list initialization from an integer initialization without Transformation:

  • Initialization is direct list initialization
  • The initializer list has only a single element
  • Enumeration is a scoped enumeration or unscoped enumeration with a fixed underlying type
  • Convert to non-narrowed conversion

is_enum and underlying_type

Since enumeration types are now strongly typed, the standard library also has a special type check to detect them:

#include <iostream>
#include <type_traits>
 
class A {};
 
enum E {};
 
enum class Ec : int {};
 
int main() {
    std::cout << std::boolalpha;
    std::cout << std::is_enum<A>::value << '\n';
    std::cout << std::is_enum<E>::value << '\n';
    std::cout << std::is_enum<Ec>::value << '\n';
    std::cout << std::is_enum<int>::value << '\n';
}

// Output
false
true
true
false

And you can use std::underlying_type or std::underlying_type_t to extract the corresponding underlying type. Examples are as follows:

#include <iostream>
#include <type_traits>
 
enum e1 {};
enum class e2 {};
enum class e3: unsigned {};
enum class e4: int {};
 
int main() {
 
  constexpr bool e1_t = std::is_same_v< std::underlying_type_t<e1>, int >;
  constexpr bool e2_t = std::is_same_v< std::underlying_type_t<e2>, int >;
  constexpr bool e3_t = std::is_same_v< std::underlying_type_t<e3>, int >;
  constexpr bool e4_t = std::is_same_v< std::underlying_type_t<e4>, int >;
 
  std::cout
    << "underlying type for 'e1' is " << (e1_t ? "int" : "non-int") << '\n'
    << "underlying type for 'e2' is " << (e2_t ? "int" : "non-int") << '\n'
    << "underlying type for 'e3' is " << (e3_t ? "int" : "non-int") << '\n'
    << "underlying type for 'e4' is " << (e4_t ? "int" : "non-int") << '\n'
    ;
}

Possible output:

underlying type for 'e1' is non-int
underlying type for 'e2' is int
underlying type for 'e3' is non-int
underlying type for 'e4' is int

cxx20

using enum

Enumeration classes can be used in cxx20. This may be a very important feature. When we want to stringify enumerations, we may need an expandable list of enumerations.

using enum Xxx can reduce the namespace of enumeration classes:

void foo(Color c)
  using enum Color;
  switch (c) {
    case Red: ...;
    case Green: ...;
    case Blue: ...;
    // etc
  }
}

However, for the early code (cxx11..cxx17), you must use the fully qualified name:

void foo(Color c)
  switch (c) {
    case Color::Red: ...;
    case Color::Green: ...;
    case Color::Blue: ...;
    // etc
  }
}

Which is better?

I prefer to support the fully qualified name method, it seems formal and clear, and that a little bit of trouble when typing, the general IDE can automatically complete so there is no problem.

In special scenarios, this feature of cxx20 may be very critical, but since it is probably impossible for you to encounter it, I will not explain how this scenario is rare, how it cannot be solved, and how it is forced to use other means.

cxx23

std::is_scoped_enum and std::to_underlying

Continue to add type check std::is_scoped_enum in cxx23, there is nothing to say.

In addition, there is std::to_underlying, you don't need to use static_cast anymore. It's really useless, so I might as well write static_cast.

Enhance it: AWESOME_MAKE_ENUM

One obvious place is the stringification of enumerations.

Hand stick

A simple hand stroke can be like this:

#include <iostream>
#include <string>
#include <chrono>

using std::cout; using std::cin;
using std::endl; using std::string;

enum Fruit { Banana, Coconut, Mango, Carambola, Total } ;
static const char *enum_str[] =
        { "Banana Pie", "Coconut Tart", "Mango Cookie", "Carambola Crumble" };

string getStringForEnum( int enum_val )
{
    string tmp(enum_str[enum_val]);
    return tmp;
}

int main(){
    string todays_dish = getStringForEnum(Banana);
    cout << todays_dish << endl;

    return EXIT_SUCCESS;
}

Tripartite library

In addition, there is already a more mature comprehensive support at Neargye/magic_enum , which uses interesting techniques to provide additional support for various enum classes, such as enum_cast, enum_name, enum_value, enum_values and so on. He also fully showed you how abnormal C++'s cross-compiler capabilities are.

AWESOME_MAKE_ENUM

If you do not want to integrate magic_enum so full of ability, but only need a simple amount literal map, then we HICC-cxx / CMDR-cxx provides a macro AWESOME_MAKE_ENUM (based Debdatta Basu provided Version), with it you can get a literal representation of the enumeration at a very small cost.

#include <cmdr/cmdr_defs.hh>

/* enum class Week {
  Sunday, Monday, Tuesday, 
  Wednesday, Thursday, Friday, Saturday
}; */
AWESOME_MAKE_ENUM(Week,
                  Sunday, Monday, Tuesday, 
                  Wednesday, Thursday, Friday, Saturday);

std::cout << Week::Saturday << '\n';
// Output:
// Week::Saturday

AWESOME_MAKE_ENUM(Animal
                  DOG,
                  CAT = 100,
                  HORSE = 1000
};

auto dog = Animal::DOG;
std::cout << dog << '\n';
std::cout << Animal::HORSE << '\n';
std::cout << Animal::CAT << '\n';
// Output:
// Animal::DOG
// Animal::HORSE
// Animal::CAT

I have to admit that the implementation of AWESOME_MAKE_ENUM is relatively inefficient, which may be a price to be paid, so it should only be used in a small number of string output places. But even if it is so inefficient, it is actually nothing. As long as you don't frequently use its string output feature in high-frequency trading, it is nothing.

AWESOME_MAKE_ENUM is like this:

#define AWESOME_MAKE_ENUM(name, ...)                                \
    enum class name { __VA_ARGS__,                                  \
                      __COUNT };                                    \
    inline std::ostream &operator<<(std::ostream &os, name value) { \
        std::string enumName = #name;                               \
        std::string str = #__VA_ARGS__;                             \
        int len = str.length(), val = -1;                           \
        std::map<int, std::string> maps;                            \
        std::ostringstream temp;                                    \
        for (int i = 0; i < len; i++) {                             \
            if (isspace(str[i])) continue;                          \
            if (str[i] == ',') {                                    \
                std::string s0 = temp.str();                        \
                auto ix = s0.find('=');                             \
                if (ix != std::string::npos) {                      \
                    auto s2 = s0.substr(ix + 1);                    \
                    s0 = s0.substr(0, ix);                          \
                    std::stringstream ss(s2);                       \
                    ss >> val;                                      \
                } else                                              \
                    val++;                                          \
                maps.emplace(val, s0);                              \
                temp.str(std::string());                            \
            } else                                                  \
                temp << str[i];                                     \
        }                                                           \
        std::string s0 = temp.str();                                \
        auto ix = s0.find('=');                                     \
        if (ix != std::string::npos) {                              \
            auto s2 = s0.substr(ix + 1);                            \
            s0 = s0.substr(0, ix);                                  \
            std::stringstream ss(s2);                               \
            ss >> val;                                              \
        } else                                                      \
            val++;                                                  \
        maps.emplace(val, s0);                                      \
        os << enumName << "::" << maps[(int) value];                \
        return os;                                                  \
    }

It needs __VA_ARGS__ variable parameter macro expansion capability, the following compilers (complete in here ) can support:

  • Gcc 3+
  • clang
  • Visual Studio 2005+

In addition, you also need a c++11 compiler.

Logically speaking, I could also continue to provide the function of string parse, but I thought that this was originally a fast version that will be available, so there is no need to improve it. It has been 10 years since cxx11, and there are many implementation versions of these aspects. More complete, although each has its own discomforts, there is nothing to tolerate. If you can't bear it, it is enough to use two switch cases to do forward and reverse mapping. How troublesome can it be.

Afterword

Of course, if the above two methods is still not satisfied, and they wanted to get hold of simple and comprehensive automated mapping, perhaps you can here looking for some ideas, and then self-fulfilling.

:end:


hedzr
95 声望19 粉丝