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;