首发于 C++杂谈

enum和std::string的自动互相转换

C++ 中枚举类型 enum class 和字符串的互转一直是绕不开的痛点,手动写互转函数很单调乏味。一些支持数据序列化的库例如 protobuffer 自动生成相关代码,但是这些库一般都相当的大而重,还需要调用额外的程序去生成C++代码,显得繁琐。万幸的是,超轻量级的单头文件库 magic_enum 解决了这些问题,甚至提供了更多,我们将会学习它是如何解决这些问题的,并且模仿它写一个有类似功能的仅有 150 行代码的最小实现, 可以 编译期求值

最终效果

这是我的最小实现的最终效果:

enum class Color : int
  RED = -2,
  BLUE = 0,
  GREEN = 2
int main()
  const auto pretty_print = [](const std::string &name, const auto &array) {
    std::cout << name << ": [";
    for (const auto &value : array)
      std::cout << value << ", ";
    std::cout << "]" << std::endl;
  pretty_print("Valid Values", enum_names_v<Color>);
  pretty_print("Valid Names", enum_values_v<Color>);
  static_assert(string2enum<Color>("RED") == Color::RED);
  static_assert(string2enum<Color>("BLUE") == Color::BLUE);
  static_assert(string2enum<Color>("GREEN") == Color::GREEN);
  // Compile Error, LOL!
  // static_assert(string2enum<Color>("GRAY") == Color::GREEN);
  std::cout << "RED: " << static_cast<int>(string2enum<Color>("RED")) << std::endl;
  std::cout << "BLUE: " << static_cast<int>(string2enum<Color>("BLUE")) << std::endl;
  std::cout << "GREEN: " << static_cast<int>(string2enum<Color>("GREEN")) << std::endl;
  // throw exception, LOL!
  // std::cout << "GREEN: " << static_cast<int>(string2enum<Color>("GRAY")) << std::endl;
  static_assert(enum2string<Color>(static_cast<Color>(-2)) == "RED");
  static_assert(enum2string<Color>(static_cast<Color>(0)) == "BLUE");
  static_assert(enum2string<Color>(static_cast<Color>(2)) == "GREEN");
  // Compile Error, LOL!
  // static_assert(enum2string<Color>(static_cast<Color>(4)) == "GRAY");
  std::cout << "-2 : " << enum2string<Color>(static_cast<Color>(-2)) << std::endl;
  std::cout << "0 : " << enum2string<Color>(static_cast<Color>(0)) << std::endl;
  std::cout << "2 : " << enum2string<Color>(static_cast<Color>(2)) << std::endl;
  // throw exception, LOL!
  // std::cout << "1 : " << enum2string<Color>(static_cast<Color>(1)) << std::endl;
  return 0;

代码输出:

Valid Values: [-2, 0, 2, ]
Valid Names: [RED, BLUE, GREEN, ]
RED: -2
BLUE: 0
GREEN: 2
-2 : RED
0 : BLUE
2 : GREEN

模仿magic_enum的最小实现

真TMD太神奇了,我们来看它到底是怎么做到的。我们主要针对C++17和clang-10.0分析。

如何获取枚举类型名

C/C++标准规定了预定义宏,例如 __FILE__ __LINE__ __FUNCTION__ 等等,编译器提供了这些宏的实现。但是这些宏是不够的,各家编译器还有其他的宏定义比如 __PRETTY_FUNCTION__ 。如果能够将类型信息嵌入这些宏中,我们就可以得到类型的名字啦。 __PRETTY_FUNCTION__ 提供了这个功能。下面程序可以看到。

template <typename T>
void func(T c){
    std::cout << "__PRETTY_FUNCTION__ value: {" << __PRETTY_FUNCTION__ << "} __PRETTY_FUNCTION__ address: ";
    std::cout.operator<<(__PRETTY_FUNCTION__) << std::endl;
    // print const char* address
    std::cout << "__FUNCTION__ value: {" << __FUNCTION__ << "} __FUNCTION__ address: ";
    std::cout.operator<<(__FUNCTION__) << std::endl;
int main() {
    func<int>(1);
    func<char>(1);
// 输出:
// __PRETTY_FUNCTION__ value: {void func(T) [T = int]} __PRETTY_FUNCTION__ address: 0x400ae1
// __FUNCTION__ value: {func} __FUNCTION__ address: 0x400b2e
// __PRETTY_FUNCTION__ value: {void func(T) [T = char]} __PRETTY_FUNCTION__ address: 0x400b4c
// __FUNCTION__ value: {func} __FUNCTION__ address: 0x400b2e

从上面我们可以看到, __PRETTY_FUNCTION__ 提供了模版参数类的名字,它是 static const char* , 这就意味着我们可以通过模版函数来得到类名。 magic_enum 就是这么做的,但是它返回的类名字符串不是一个全局唯一的静态变量,而是一个常量(细节,不影响理解)。

enum class Color { RED = -2, BLUE = 0, GREEN = 2 };
const std::string_view name1 = magic_enum::detail::n<Color>();
std::cout << name1 <<": ";
std::cout.operator<<(&name1[0]) << std::endl;
const std::string_view name2 = magic_enum::detail::n<Color>();
std::cout << name2 <<": ";
std::cout.operator<<(&name2[0]) << std::endl;
// 输出:
// Color: 0x7ffc7fff3050
// Color: 0x7ffc7fff3018

如何获得枚举类型变量的名字

给定一个枚举变量,输出其名字的字符串,比如 Color::RED 的输出是 "RED" 。本质上就是给定一个枚举变量底层的整数类型值,输出其名字,比如 Color::RED 的值为 -2 ,我们要输出 -2 Color 中的名字。 __PRETTY_FUNCTION__ 又提供了帮助。

enum class Color : int { RED = -2, BLUE = 0, GREEN = 2 };
template<typename E, E V>
void func() {
  std::cout << __PRETTY_FUNCTION__ << std::endl;
func<Color, static_cast<Color>(-2)>();
func<Color, static_cast<Color>(1)>();
// 输出
// void func() [E = Color, V = Color::RED]
// void func() [E = Color, V = 1]

整数值 2 有效,我们看到了 "RED" , 如果给定的整数值是非法,则只有数值。我们用它来实现获取值名。下面给出一个简易实现。

// 该函数可以分辨
// bool is_valid() [E = Color, V = Color::GREEN]
// bool is_valid() [E = Color, V = 1]
template <typename E, E V>
constexpr std::string_view get_enum_value_name(){
  std::string_view name{__PRETTY_FUNCTION__, sizeof(__PRETTY_FUNCTION__) - 2};
  for (std::size_t i = name.size(); i > 0; --i) {
    if (!((name[i - 1] >= '0' && name[i - 1] <= '9') ||
          (name[i - 1] >= 'a' && name[i - 1] <= 'z') ||
          (name[i - 1] >= 'A' && name[i - 1] <= 'Z') ||
          (name[i - 1] == '_'))) {
      name.remove_prefix(i);
      break;
  if (name.size() > 0 && ((name.front() >= 'a' && name.front() <= 'z') ||
                          (name.front() >= 'A' && name.front() <= 'Z') ||
                          (name.front() == '_'))) {
    return name;
  return {}; // Invalid name.
// 判断某个数值是否为有效的枚举类型值
template <typename E, E V>
constexpr bool is_valid()
  // get_enum_value_name来自于上面一小节。
  return get_enum_value_name<E, V>().size() != 0;
std::cout << "Name: [" <<  get_enum_value_name<Color, static_cast<Color>(2)>() << "]" << std::endl;
std::cout << "Name: [" << get_enum_value_name<Color, static_cast<Color>(1)>() << "]"  << std::endl;
// 输出
// Name: [GREEN]
// Name: []  

如何获取枚举类型中所有值的字符串名

简单想法就是枚举 numeric_limit<T>::min() numeric_limit<T>::max() ,查每个数值是否有效,如果有效就给出名字。 magic_enum 默认从 -128 128

template <typename E, int... I>
constexpr auto get_all_valid_names()
  constexpr std::array<std::string_view, sizeof...(I)> names{get_enum_value_name<E, static_cast<E>(I)>()...};
  constexpr std::size_t count = [](decltype((names)) names_) constexpr noexcept->std::size_t
    auto count_ = std::size_t{0};
    for (std::size_t i_ = 0; i_ < names_.size(); ++i_)
      if (names_[i_].size() != 0)
        ++count_;
    return count_;
  (names);
  std::array<std::string_view, count> valid_names{};
  for (std::size_t i = 0, v = 0; i < names.size(); ++i)
    if (names[i].size() != 0)
      valid_names[v++] = names[i];
  return valid_names;
int main()
  const auto valid_names = get_all_valid_names<Color, -3, -2, -1, 0, 1, 2, 3>();
  pretty_print("Valid Names", valid_names);
  return 0;
// 输出
// Valid Names: [RED, BLUE, GREEN, ]

至此,几乎所有需要的功能都已具备,我们只需添加一下辅助函数就可以完成这个最小实现,具体实现代码最附录中。

附录

Enum和String互转的最小实现, 最终版, 可以编译期求值。在 wandbox 的gcc9.3.0和clang-10.0.1上均可执行。

#include <iostream>
#include <array>
#include <exception>
#include <stdexcept>
#include <string_view>
#if defined(__clang__)
#define PRETTY_FUNCTION_NAME __PRETTY_FUNCTION__
#define OFFSET 2
#elif defined(__GNUC__)
#define PRETTY_FUNCTION_NAME __PRETTY_FUNCTION__
#define OFFSET 51
#elif defined(_MSC_VER)
#define PRETTY_FUNCTION_NAME __FUNCSIG__
#define OFFSET 17
#endif
// 该函数可以分辨
// bool is_valid() [E = Color, V = Color::GREEN]
// bool is_valid() [E = Color, V = 1]
template <typename E, E V>
constexpr std::string_view get_enum_value_name()
  std::string_view name{PRETTY_FUNCTION_NAME, sizeof(PRETTY_FUNCTION_NAME) - OFFSET};
  for (std::size_t i = name.size(); i > 0; --i)
    if (!((name[i - 1] >= '0' && name[i - 1] <= '9') ||
          (name[i - 1] >= 'a' && name[i - 1] <= 'z') ||
          (name[i - 1] >= 'A' && name[i - 1] <= 'Z') ||
          (name[i - 1] == '_')))
      name.remove_prefix(i);
      break;
  if (name.size() > 0 && ((name.front() >= 'a' && name.front() <= 'z') ||
                          (name.front() >= 'A' && name.front() <= 'Z') ||
                          (name.front() == '_')))
    return name;
  return {}; // Invalid name.
// 该函数可以分辨
// bool is_valid() [E = Color, V = Color::GREEN]
// bool is_valid() [E = Color, V = 1]
template <typename E, E V>
constexpr bool is_valid()
  // get_enum_value_name来自于上面一小节。
  return get_enum_value_name<E, V>().size() != 0;
// 制造std::integer_sequence,在[-value,value]之间
template <int... Is>
constexpr auto make_integer_list_wrapper(std::integer_sequence<int, Is...>)
  constexpr int half_size = sizeof...(Is) / 2;
  return std::integer_sequence<int, (Is-half_size)...>();
// 编译器已知的测试integer sequence
constexpr auto test_integer_sequence_v = make_integer_list_wrapper(std::make_integer_sequence<int, 256>());
template <typename E, int... Is>
constexpr size_t get_enum_size(std::integer_sequence<int, Is...>)
  constexpr std::array<bool, sizeof...(Is)> valid{is_valid<E, static_cast<E>(Is)>()...};
  constexpr std::size_t count = [](decltype((valid)) valid_) constexpr noexcept->std::size_t
    auto count_ = std::size_t{0};
    for (std::size_t i_ = 0; i_ < valid_.size(); ++i_)
      if (valid_[i_])
        ++count_;
    return count_;
  (valid);
  return count;
// 一个enum class里面有几个值。
template <typename E>
constexpr std::size_t enum_size_v = get_enum_size<E>(test_integer_sequence_v);
template <typename E, int... Is>
constexpr auto get_all_valid_values(std::integer_sequence<int, Is...>)
  constexpr std::array<bool, sizeof...(Is)> valid{is_valid<E, static_cast<E>(Is)>()...};
  constexpr std::array<int, sizeof...(Is)> integer_value{Is...};
  std::array<int, enum_size_v<E>> values{};
  for (std::size_t i = 0, v = 0; i < sizeof...(Is); ++i)
    if (valid[i])
      values[v++] = integer_value[i];
  return values;
template <typename E, int... Is>
constexpr auto get_all_valid_names(std::integer_sequence<int, Is...>)
  constexpr std::array<std::string_view, sizeof...(Is)> names{get_enum_value_name<E, static_cast<E>(Is)>()...};
  std::array<std::string_view, enum_size_v<E>> valid_names{};
  for (std::size_t i = 0, v = 0; i < names.size(); ++i)
    if (names[i].size() != 0)
      valid_names[v++] = names[i];
  return valid_names;
template <typename E>
constexpr auto enum_names_v = get_all_valid_names<E>(test_integer_sequence_v);
template <typename E>
constexpr auto enum_values_v = get_all_valid_values<E>(test_integer_sequence_v);
// gcc里面不允许直接在constexpr function 最后返回throw,这是gcc的bug,
// 所以造了这个函数来绕过gcc的bug
constexpr auto static_throw(int n) -> void
  n <= 0 ? throw std::runtime_error("should not reach here. Invalid value.") : 0;
template <typename E>
constexpr E string2enum(const std::string_view str)
  constexpr auto valid_names = enum_names_v<E>;
  constexpr auto valid_values = enum_values_v<E>;
  constexpr auto enum_size = enum_size_v<E>;
  for (size_t i = 0; i < enum_size; ++i)
    if (str == valid_names[i])
      return static_cast<E>(valid_values[i]);
  static_throw(-1);
  return E{};
template <typename E>
constexpr std::string_view enum2string(E V)
  constexpr auto valid_names = enum_names_v<E>;
  constexpr auto valid_values = enum_values_v<E>;
  constexpr auto enum_size = enum_size_v<E>;
  for (size_t i = 0; i < enum_size; ++i)
    if (static_cast<int>(V) == valid_values[i])
      return valid_names[i];
  static_throw(-1);
  return "";
enum class Color : int
  RED = -2,
  BLUE = 0,
  GREEN = 2
int main()
  const auto pretty_print = [](const std::string &name, const auto &array) {
    std::cout << name << ": [";
    for (const auto &value : array)
      std::cout << value << ", ";
    std::cout << "]" << std::endl;
  const auto &valid_names = enum_names_v<Color>;
  const auto &valid_values = enum_values_v<Color>;
  pretty_print("Valid Values", valid_values);
  pretty_print("Valid Names", valid_names);
  static_assert(string2enum<Color>("RED") == Color::RED);
  static_assert(string2enum<Color>("BLUE") == Color::BLUE);
  static_assert(string2enum<Color>("GREEN") == Color::GREEN);
  // Compile Error, LOL!
  // static_assert(string2enum<Color>("GRAY") == Color::GREEN);
  std::cout << "RED: " << static_cast<int>(string2enum<Color>("RED")) << std::endl;
  std::cout << "BLUE: " << static_cast<int>(string2enum<Color>("BLUE")) << std::endl;
  std::cout << "GREEN: " << static_cast<int>(string2enum<Color>("GREEN")) << std::endl;
  // throw exception, LOL!
  // std::cout << "GREEN: " << static_cast<int>(string2enum<Color>("GRAY")) << std::endl;
  static_assert(enum2string<Color>(static_cast<Color>(-2)) == "RED");
  static_assert(enum2string<Color>(static_cast<Color>(0)) == "BLUE");
  static_assert(enum2string<Color>(static_cast<Color>(2)) == "GREEN");
  // Compile Error, LOL!
  // static_assert(enum2string<Color>(static_cast<Color>(4)) == "GRAY");
  std::cout << "-2 : " << enum2string<Color>(static_cast<Color>(-2)) << std::endl;
  std::cout << "0 : " << enum2string<Color>(static_cast<Color>(0)) << std::endl;
  std::cout << "2 : " << enum2string<Color>(static_cast<Color>(2)) << std::endl;