在 Java浙值、C# 這樣的語言中,從枚舉轉(zhuǎn)換成字符串纲爸,或者從字符串轉(zhuǎn)換成枚舉亥鸠,都是很常見的操作,也很方便识啦。比如下面是 C# 的例子:
public enum Color { red, green, blue }
static void Main(string[] args) {
Console.WriteLine("This color is {0}.", Color.red);
}
之所以可以這么用负蚊,是因?yàn)樵?IL 中以元數(shù)據(jù)方式保存了整個枚舉類型的各類信息,包括其內(nèi)部實(shí)際值和類型名稱字符串颓哮。
C++ 中就沒有那么容易了家妆,因?yàn)?C++ 直接將源代碼編譯成目標(biāo)機(jī)的機(jī)器語言,也就是最終執(zhí)行的指令序列冕茅,枚舉類型的名稱字符串在指令序列中是不存在的伤极。但是,現(xiàn)實(shí)應(yīng)用中確實(shí)可能存在這樣的場合姨伤,即需要從枚舉名稱字符串找到它對應(yīng)的枚舉值哨坪,有沒有辦法實(shí)現(xiàn)呢?
有人說這還不簡單乍楚,手工建立一個查詢字典不就可以了么当编?確實(shí)是可以,但是不得不說徒溪,這個方法它確實(shí)是既低效又丑陋忿偷,對于講究代碼美學(xué)的高等級碼農(nóng)來說,肯定是不能忍受啊臊泌,我們要的就是不管看起來還是用起來鲤桥,都無比簡潔自然的那種實(shí)現(xiàn)。
如果只從 C++ 標(biāo)準(zhǔn)來看是沒有直接辦法的渠概,但事實(shí)上每一種 C++ 編譯器都在 C++ 標(biāo)準(zhǔn)之外有所拓展茶凳,充分利用好這些拓展,就能輕松實(shí)現(xiàn)上述需求高氮。本文就是筆者在 Github 上沖浪時慧妄,無意中發(fā)現(xiàn)的一個名叫 magic_enum 的 C++ 項(xiàng)目,相當(dāng)完美地解決了這個問題剪芍。隨后筆者重新 C++20 的 concept塞淹,并使用 doctest 重新寫了一個相對簡單的示例程序。下面進(jìn)行簡要介紹和技術(shù)解析罪裹。
使用示例
先看最常用的使用場景:
enum class Color : int { RED = -10, BLUE = 0, GREEN = 10 };
//場景一:枚舉值轉(zhuǎn)換成字符串
CHECK_EQ(enum_name(Color::RED), "RED");
//場景二:字符串轉(zhuǎn)換成枚舉值
CHECK_EQ(enum_cast<Color>("BLUE").value(), Color::BLUE);
場景一饱普,從枚舉值轉(zhuǎn)換為字符串运挫,這個相對簡單,只要找到辦法能將枚舉值的表示字符串套耕,轉(zhuǎn)化為實(shí)際的字符串類型就可以谁帕。
場景二,從字符串轉(zhuǎn)換成枚舉值冯袍,這個來說要復(fù)雜得多匈挖。首先,得知道要轉(zhuǎn)換成哪一個枚舉類型康愤,因?yàn)橐粋€字符串可能與多個枚舉類型相對應(yīng)儡循,所以必須要指定轉(zhuǎn)換類型,可以用模板參數(shù)來表示征冷,就像上面例子中那樣择膝;其次,一個字符串未必一定能夠成功轉(zhuǎn)換成指定枚舉類型中的值检激,比如上面例子中如果使用 "CYAN" 來作為參數(shù)肴捉,那么是沒辦法轉(zhuǎn)換成 RED、BLUE叔收、GREEN 三者之一的齿穗,換句話說,從字符串轉(zhuǎn)換到枚舉值是有可能沒有結(jié)果的饺律。
枚舉值轉(zhuǎn)換為字符串
閑話少說缤灵,直接上代碼(簡化版):
template <typename E>
concept Enum = std::is_enum_v<E>;
template <Enum E, E V>
constexpr auto n() noexcept {
# if defined(__clang__) || defined(__GNUC__)
constexpr auto name = pretty_name({ __PRETTY_FUNCTION__, sizeof(__PRETTY_FUNCTION__) - 2 });
# elif defined(_MSC_VER)
//auto __cdecl magic_enum::detail::n<enum Color, Color::RED>(void) noexcept 去掉末尾17個再過濾開頭
constexpr auto name = pretty_name({ __FUNCSIG__, sizeof(__FUNCSIG__) - 17 });
# endif
return static_string<name.size()>{name};
}
template <Enum E, E V>
inline constexpr auto enum_name_v = n<E, V>();
理解這段代碼的關(guān)鍵,就是各種編譯器的自定義宏蓝晒。以 Visual C++ 為例,它的內(nèi)部對每個函數(shù)都有一個自定義宏 FUNCSIG帖鸦,意思差不多就是函數(shù)簽名芝薇。在 clang 或者 g++ 里就是 PRETTY_FUNCTION 宏。上面代碼中的 n() 函數(shù)里作儿,使用條件編譯判斷當(dāng)前使用的是哪個編譯器洛二,再根據(jù)不同的編譯器選擇不同的自定義宏,獲取編譯器內(nèi)部的函數(shù)簽名攻锰,再通過 pretty_name 函數(shù)截取到對應(yīng)的值名稱晾嘶。
比如我們可以使用以下用法,獲取到 Color::RED 值所對應(yīng)的名稱字符串 “RED”:
constexpr std::string_view s = enum_name_v<Color, Color::RED>;
CHECK_EQ(s, "RED");
enum_name_v 直接獲取 n 函數(shù)的返回值娶吞,那么將模板參數(shù)代入 n 函數(shù)后垒迂,在 Visual C++ 編譯器里,其函數(shù)簽名就變成了
#define __FUNCSIG__ \
"auto __cdecl magic_enum::detail::n<enum Color, Color::RED>(void) noexcept"
pretty_name 函數(shù)的調(diào)用參數(shù)只有一個妒蛇,就是 string_view机断,花括號內(nèi)是它的構(gòu)造參數(shù)楷拳,將長度減去 17 之后(包含末尾的 \0),實(shí)際調(diào)用的參數(shù)值就成了:
"auto __cdecl magic_enum::detail::n<enum Color, Color::RED"
pretty_name 函數(shù)的作用吏奸,就是由后向前掃描整個字符串欢揖,一旦發(fā)現(xiàn)非標(biāo)識符字符就停止,然后截斷已經(jīng)掃描過的字符串并返回:
constexpr std::string_view pretty_name(std::string_view name) noexcept {
for (std::size_t i = name.size(); i > 0; --i) {
if (!((name[i - 1] >= '0' && name[i - 1] <= '9') || (name[i - 1] == '_') ||
(name[i - 1] >= 'a' && name[i - 1] <= 'z') || (name[i - 1] >= 'A' && name[i - 1] <= 'Z'))) {
name.remove_prefix(i); //由后向前奋蔚,發(fā)現(xiàn)非標(biāo)識符字符即啟動截斷她混,保留后半截
break;
}
}
if (name.size() > 0 && ((name.front() >= 'a' && name.front() <= 'z') ||
(name.front() >= 'A' && name.front() <= 'Z') || (name.front() == '_'))) {
return name; //首字母不是數(shù)字
}
return {}; //否則就是非法名稱
}
因此,pretty_name 最后返回的就是 "RED" 這個枚舉值名稱泊碑,它向外傳遞到 enum_name_v 再賦值給 s坤按,中間經(jīng)過了自定義類型 static_string 和 string_view 兩個類型的自動轉(zhuǎn)換。所以蛾狗,最后我們的測試斷言 CHECK_EQ 是順利通過的晋涣。
還要注意的一點(diǎn)就是,從 enum_name_v 到 pretty_name 這層層調(diào)用的一系列函數(shù)沉桌,全部都是標(biāo)記了 constexpr 的谢鹊,這就意味著它們都可以在編譯期就完成求值。換句話說留凭,上面的調(diào)用在經(jīng)過編譯器處理后佃扼,最后實(shí)際變成的是以下代碼:
//這是我們原來書寫的代碼
constexpr std::string_view s = enum_name_v<Color, Color::RED>;
CHECK_EQ(s, "RED");
//這相當(dāng)于編譯器最后生成的代碼
CHECK_EQ("RED"sv, "RED");
這就是現(xiàn)代 C++ 編譯器,編譯期計算的能力已經(jīng)相當(dāng)強(qiáng)大蔼夜,由它生成的代碼兼耀,毫無疑問其執(zhí)行效率要遠(yuǎn)高于 Java、C# 以及 Python 等語言求冷。當(dāng)然瘤运,前提是首先得能熟練地掌握它。
字符串轉(zhuǎn)換為枚舉
如前所述匠题,將字符串轉(zhuǎn)換為枚舉要麻煩許多拯坟。針對所轉(zhuǎn)換的枚舉類型,必須得要有一個完備的字符串列表韭山,并與枚舉值一一對應(yīng)郁季,這樣才可以根據(jù)字符串去進(jìn)行查找。那么钱磅,需要準(zhǔn)備哪些數(shù)據(jù)呢梦裂?來作一下具體分析:
第一步,要有一個合法枚舉值列表盖淡,并且編譯器要能根據(jù)普通的枚舉聲明自動列舉出來年柠。這里需要注意的是,枚舉值是可以從負(fù)數(shù)開始的禁舷,也可以是稀疏的彪杉,就像前面的例子毅往,Color 類型的三個枚舉值,對應(yīng)的內(nèi)部值分別是 -10派近、0攀唯、10。
為進(jìn)一步簡化示例代碼渴丸,先不考慮標(biāo)志位枚舉的情況侯嘀,假定都是如 Color 這樣的簡單枚舉,取枚舉值列表可以這樣完成:
//V是否為指定枚舉的合法值
template <Enum E, auto V>
constexpr bool is_valid() noexcept { return n<E, static_cast<E>(V)>().size() != 0; }
//返回以O(shè)為基準(zhǔn)谱轨、指定序號的枚舉值
template <Enum E, int O, typename U = std::underlying_type_t<E>>
constexpr E value(std::size_t i) noexcept { return static_cast<E>(static_cast<int>(i) + O); }
template <Enum E, int Min, std::size_t... I>
constexpr auto values(std::index_sequence<I...>) noexcept {
//遍歷指定取值檢查是否合法枚舉值
constexpr bool valid[sizeof...(I)] = { is_valid<E, value<E, Min, IsFlags>(I)>()... };
constexpr std::size_t count = values_count(valid); //共有多少個合法枚舉值
if constexpr (count > 0) {
E values[count] = {};
for (std::size_t i = 0, v = 0; v < count; ++i) //將所有合法枚舉值填充入數(shù)組
if (valid[i])
values[v++] = value<E, Min, IsFlags>(i);
return std::to_array(values); //再轉(zhuǎn)換成array后返回
} else {
return std::array<E, 0>{}; //無合法枚舉值戒幔,返回空array
}
}
//返回取值范圍中的所有合法值,是一個基于最小值的索引序列
template <Enum E, typename U = std::underlying_type_t<E>>
constexpr auto values() noexcept {
constexpr auto min = reflected_min_v<E>; //枚舉范圍最小值
constexpr auto max = reflected_max_v<E>; //枚舉范圍最大值
constexpr auto range_size = max - min + 1;
return values<E, IsFlags, reflected_min_v<E>>(std::make_index_sequence<range_size>{});
}
上面例子中土童,reflected_min_v 和 reflected_max_v 兩個模板函數(shù)诗茎,是根據(jù)枚舉內(nèi)部類型值以及用戶自定義設(shè)定,來確定枚舉值的取值范圍献汗。在遍歷整個取值范圍后敢订,將所有合法的枚舉值存入一個 std::array。注意這里所有函數(shù)仍然都是帶 constexpr 標(biāo)記的罢吃。
第二步楚午,要有一個枚舉值字符串列表,與上面的合法枚舉值一一對應(yīng)尿招。這個相對好辦矾柜,解決了第一步之后,可以依次遍歷每個枚舉值生成字符串就谜,組成列表就可以了:
template <Enum E>
inline constexpr auto count_v = values_v<E>.size(); //size_t類型
template <Enum E, std::size_t... I>
constexpr auto names(std::index_sequence<I...>) noexcept {
return std::array<std::string_view, sizeof...(I)>{ { enum_name_v<E, values_v<E>[I]>... }};
}
template <Enum E>
inline constexpr auto names_v = names<E>(std::make_index_sequence<count_v<E>>{});
下面可以基本完成 enum_cast 主功能了:
template <Enum E>
constexpr auto enum_cast(std::string_view value) noexcept -> std::optional<E>
{
for (std::size_t i = 0; i < count_v<E>; ++i) //逐個比較怪蔑,相等則返回對應(yīng)枚舉值
if (value == names_v<E>[i])
return enum_value<E>(i);
return {};
}
注意返回的是 std::optional 模板類型,如果對應(yīng)的枚舉值沒有找到丧荐,則返回空值饮睬。
更進(jìn)一步的設(shè)計
上文中我們完全沒有考慮標(biāo)志位枚舉的情況,這種情況要復(fù)雜得多篮奄,看以下的使用示例:
enum class AnimalFlags : std::uint64_t {
HasClaws = 1 << 10,
CanFly = 1 << 20,
EatsFish = 1 << 30,
Endangered = std::uint64_t{ 1 } << 40
};
constexpr AnimalFlags f1 = AnimalFlags::HasClaws | AnimalFlags::EatsFish;
CHECK_EQ(enum_name(f1), "HasClaws|EatsFish");
constexpr auto f2 = magic_enum::flags::enum_cast<AnimalFlags>("EatsFish|CanFly");
CHECK_EQ(f2.value(), AnimalFlags::EatsFish | AnimalFlags::CanFly);
還有最常用的流操作符:
std::ostringstream str;
str << Color::RED;
CHECK_EQ(str.str(), "RED");
此外,還應(yīng)當(dāng)允許用戶自定義枚舉值的字符串名稱割去、自定義字符串比較算法等等窟却,作為一個相對完整的功能,這些都是必要的呻逆。具體的實(shí)現(xiàn)本文就不再詳述了夸赫,感興趣的可以點(diǎn)擊 這里 查看筆者改寫的源碼,也可以點(diǎn)擊 magic_enum 查看原始項(xiàng)目的完整源碼咖城。
歡迎關(guān)注微信公眾號茬腿,一起交流