乐趣区

关于c++:C-的枚举类型

Prologue: C++ 中的枚举类型利用以及转换到字符串的加强:AWESOME_MAKE_ENUM,…
Original From: HERE

因为长期发现须要一个枚举量到字符串的转换器,所以罗唆梳理了一遍古往今来的枚举类型的变动。

于是奇怪的冷常识又减少了。

枚举类型

enum

在 cxx11 之前,C/C++ 通过 enum 关键字申明枚举量。

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

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

从 cxx11 起,enum 容许应用不同于 integer 的其它数据类型。此时它的语法是这样的:

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

所以在必要时能够:

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

但类型并不容许大过 int,受制于 CPU 字长。所以类型的反对简直没有任何实用价值,不晓得那堆人怎么想的,看来嵌入式真的很火。

enum class

从 cxx11 起,咱们被举荐放弃 enum 改用有作用域的 enum class,也或者 enum struct。这时候申明枚举类型的形式如下:

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

同样也反对 : 类型 的类型限定,同样地,没什么用途。

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

区别

enum class 与 enum 的不同之处在于作用域受限以及强类型限定。

作用域受限次要体现在 class/struct 中的 enum class 被其外围所限定。上面的例子能够简略地阐明:

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
}

这对于内外同名的状况有很好的反对。

强类型限定当初回绝枚举量与 int 之间的隐式强制与调换,但反对 static_cast<int>(enum-value) 的形式获取到枚举量的底层 int 值。另外,枚举在满足下列条件时都能用列表初始化从一个整数初始化而无需转型:

  • 初始化是间接列表初始化
  • 初始化器列表仅有单个元素
  • 枚举是底层类型固定的有作用域枚举或无作用域枚举
  • 转换为非窄化转换

is_enum 和 underlying_type

因为枚举类型当初是强类型了,所以规范库也有专用的 type check 对其进行检测:

#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

并且能够用 std::underlying_type 或 std::underlying_type_t 来抽出相应的底层类型。示例如下:

#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'
    ;
}

可能的输入:

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

在 cxx20 中枚举类可能被 using。这有可能是一个很重要的个性,当咱们想要字符串化枚举量时可能须要一个可开展的枚举量列表。

using enum Xxx 可能削减枚举类的名字空间:

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

然而对于晚期(cxx11..cxx17)的代码来说,你必须应用全限定名:

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

孰优孰劣呢?

我比较支持全限定名形式,它显得正规清晰,而那一点点键入时的麻烦个别的 IDE 都能够主动补全所以无问题。

在非凡场景中 cxx20 的这个个性可能是十分要害的,但因为大抵你不可能遇到,所以我也就不解释这种场景如何难得、如何无奈解决、如何被迫采纳其它伎俩了。

cxx23

std::is_scoped_enum 和 std::to_underlying

在 cxx23 中持续新增类型查看 std::is_scoped_enum,这没什么好说的。

此外就是 std::to_underlying 了,你能够不用应用 static_cast 了。真的没什么卵用,我特么还不如写 static_cast 呐。

对其加强:AWESOME_MAKE_ENUM

一个不言而喻的场合就是枚举量的字符串化了。

手撸

简略的手撸能够这样:

#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;
}

三方库

另外,曾经有一个较成熟的全面的撑持在 Neargye/magic_enum,他应用了乏味的技术来提供各种各样的 enum class 的额定反对,诸如 enum_cast, enum_name, enum_value, enum_values 等等。他也向你充沛地展现了 c++ 的跨编译器能力是如许的变态。

AWESOME_MAKE_ENUM

如果你不想集成 magic_enum 那么全面的能力,而是仅仅只须要一个简略的字面量映射的话,咱们在 hicc-cxx/cmdr-cxx 中提供了一个宏 AWESOME_MAKE_ENUM(基于 Debdatta Basu 提供的版本),用它的话你能够以很大量的代价取得枚举量的字面量示意。

#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

我得抵赖,AWESOME_MAKE_ENUM 的实现是比拟低效率的,这可能是不得不付出的代价,所以它应该只被用在大量的字符串输入场合。但哪怕它是那么的低效,其实也不算什么,只有你不在高频交易中频繁地应用它的字符串输出特性,那就算不得个什么。

AWESOME_MAKE_ENUM 的实现是这样的:

#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;                                                  \
    }

它须要 __VA_ARGS__ 这种变参宏开展能力,以下编译器(残缺在 这里)都能反对:

  • Gcc 3+
  • clang
  • Visual Studio 2005+

此外,你也须要 c++11 编译器。

从情理上讲,我本也能够持续提供字符串 parse 的性能,但想到这原本就是一个将就的疾速版本,也就没必要欠缺了,cxx11 以来 10 年了,这些方面有很多实现版本都是较为欠缺的,虽说各有各的不适之处,但也没什么不能忍,不能忍就手撸两个 switch case 做正反向映射就足够了,能有多麻烦呢。

后话

当然,如果下面两种办法仍不满足,而且又很想弄个简略而 全面 的自动化映射,或者你能够在 这里 寻找一些思路,而后自行实现。

退出移动版