Modern C++ 中枚舉與字符串轉(zhuǎn)換技巧

在 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)注微信公眾號茬腿,一起交流
兆華雜記
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末呼奢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子切平,更是在濱河造成了極大的恐慌握础,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悴品,死亡現(xiàn)場離奇詭異禀综,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)苔严,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進(jìn)店門定枷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人届氢,你說我怎么就攤上這事欠窒。” “怎么了退子?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵岖妄,是天一觀的道長。 經(jīng)常有香客問我絮供,道長衣吠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任壤靶,我火速辦了婚禮缚俏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贮乳。我一直安慰自己忧换,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布向拆。 她就那樣靜靜地躺著亚茬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浓恳。 梳的紋絲不亂的頭發(fā)上刹缝,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天,我揣著相機(jī)與錄音颈将,去河邊找鬼梢夯。 笑死,一個胖子當(dāng)著我的面吹牛晴圾,可吹牛的內(nèi)容都是我干的颂砸。 我是一名探鬼主播,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼人乓!你這毒婦竟也來了勤篮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤色罚,失蹤者是張志新(化名)和其女友劉穎碰缔,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體保屯,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡手负,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了姑尺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竟终。...
    茶點(diǎn)故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖切蟋,靈堂內(nèi)的尸體忽然破棺而出统捶,到底是詐尸還是另有隱情,我是刑警寧澤柄粹,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布喘鸟,位于F島的核電站,受9級特大地震影響驻右,放射性物質(zhì)發(fā)生泄漏什黑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一堪夭、第九天 我趴在偏房一處隱蔽的房頂上張望愕把。 院中可真熱鬧,春花似錦森爽、人聲如沸恨豁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽橘蜜。三九已至,卻和暖如春付呕,著一層夾襖步出監(jiān)牢的瞬間计福,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工徽职, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留棒搜,地道東北人。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓活箕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子育韩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,937評論 2 361

推薦閱讀更多精彩內(nèi)容