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 做正反向映射就足夠了,能有多麻煩呢。
後話
當然,如果上面兩種方法仍不滿足,而且又很想弄個簡單而全面的自動化映射,或許你可以在 這裏 尋找一些思路,然後自行實現。