C++ Enum Reflection
1. 概述
简而言之,编程语言中的反射(Reflection)指的是从运行时中获取语言本身的类型等信息。C++ 缺乏这样的机制,对于最简单的 enum 类型,我们或许可以实现带有反射功能的 enum。
我们实现了几个宏,通过宏定义的 enum,就自动地拥有反射功能。
2. 用法
2.2 宏定义
1 2 3 4 5 6 7 |
// 可在任意 namespace 中调用,不可在 struct/class 内调用 #define TERARK_ENUM_PLAIN(EnumType, IntRep, ...) details... #define TERARK_ENUM_CLASS(EnumType, IntRep, ...) details... // 可在 struct/class 内调用,不可在任意 namespace 中调用 #define TERARK_ENUM_PLAIN_INCLASS(EnumType, IntRep, ...) details... #define TERARK_ENUM_CLASS_INCLASS(EnumType, IntRep, ...) details... |
3. 支持的功能
3.1 函数
支持的函数都定义在全局 namespace 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template<class Enum> terark::fstring enum_name(Enum v); template bool enum_value(const terark::fstring& name, Enum* result); /// for convenient template<class Enum> Enum enum_value(const terark::fstring& name, Enum Default); // use case: // enum_for_each([](terark::fstring name, Enum val){...}); template<class Enum> void enum_for_each(Func fn); template<class Enum> std::string enum_str_all_names(); |
3.2 举例说明:
1 2 3 4 5 6 7 |
#include <terark/util/enum.hpp> // 在 namespace 中调用该宏,不能在 class/struct 内调用 TERARK_ENUM_CLASS(MyEnum, char, Value1, Value2 = (SomeTemplate<1,2>::value), Value3 = 30 // 限制:这里不能有逗号 ) |
上面的宏展开会生成以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// 宏展开的 enum 定义 enum class MyEnum : int { Value1, Value2 = (SomeTemplate<1,2>::value), Value3 = 30 }; // 宏展开的反射功能: int enum_rep_type(MyEnum*); inline terark::fstring enum_str_define(MyEnum*) { return "enum class MyEnum : int" " { Value1, Value2 = (SomeTemplate<1,2>::value), Value3 = 30 }"; } inline std::pair<const terark::fstring*, size_t> enum_all_names(MyEnum*) { static const terark::fstring s_names[] = { terark::var_symbol("Value1"), terark::var_symbol("Value2 = (SomeTemplate<1,2>::value)"), terark::var_symbol("Value3 = 30") }; return std::make_pair(s_names, sizeof(s_names)/s_names[0]); } inline const MyEnum* enum_all_values(MyEnum*) { static const MyEnum s_values[] = { EnumValueInit() - MyEnum::Value1, EnumValueInit() - MyEnum::Value2 = (SomeTemplate<1,2>::value), EnumValueInit() - MyEnum::Value3 = 30 }; return s_values; } |
4. 应用场景
最典型的应用场景莫过于处理配置信息,把用户配置的字符串,转化为 Enum 值,写 Log 时,又把 Enum 转化为字符串。例如 RocksDB 中就有大量此类场景。
目前,该 enum reflection 已经向 RocksDB 提交为 Pull Request,用来改善 RocksDB 中大量手工实现的 enum reflection(样例)。
5. 实现细节
为了突出重点,仅说明实现中的几个关键点。
5.1 s_name 与 s_value
s_name 与 s_value 是平行数组,name 为 s_name[i] 的 enum,其值为 s_value[i]。这两个平行数组几乎可以用来实现所有的反射功能,它们分别在 enum_all_names 和 enum_all_values 中定义。
关键点是通过宏展开如何生成 s_name 与 s_value。
5.2 宏 TERARK_PP_MAP(map,ctx,...)
遍历该宏的变参列表,生成一个结果列表,该宏的实现包含了一点奇技淫巧,但限制变参列表长度最大为 61(Visual C++ 最多支持 127 个宏参数,gcc 支持近乎无限个宏参数)。
5.3 EnumValue = SomeValue 是一个整体
EnumName = SomeValue 这样的语法结构,作为宏参数时,它是一个整体,可以把它变成一个字符串 “EnumName = SomeValue” ,除此之外,无法对它进行其它操作(我们期望的拆解)。
5.4 s_name 的初始化
作为 enum 的 name,在
EnumName = SomeValue 中,我们只需要 EnumName,这个比较容易处理,我们实现了一个 var_symbol 函数,可以从中把 EnumName 切分出来。
在 s_name 的初始化列表中,我们利用 TERARK_PP_MAP,逐个调用 var_symbol 函数,生成 EnumName。所以,相比 s_value 的初始化,s_name 的初始化是比较简单的。
5.5 s_value 的初始化
s_value 的初始化中也要处理
EnumName = SomeValue ,因为要获取 EnumName 的值,而不是其字符串形式,我们要处理的就是
EnumName = SomeValue 的整个语法结构,其中
= SomeValue 是可选的,所以我们应该只保留 EnumName ,而删去
= SomeValue ,这个需求在预处理器中无法完成。
我们就只有想办法利用 C++ 的语法,实现 删去
= SomeValue 的功能,可以利用操作符重载来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template<class Enum> class EnumValueInit { Enum val; public: operator Enum() const { return val; } /// set val EnumValueInit& operator-(Enum v) { val = v; return *this; } /// absorb the IntRep param template<class IntRep> EnumValueInit& operator=(IntRep) { return *this; } }; |
这样,有了 EnumValueInit,我们就可以定义一个表达式,其接受 EnumName 或者 EnumName = SomeValue ,产生的值总是 EnumName。这个表达式就是:
EnumValueInit() - EnumName = SomeValue
在这里, EnumValueInit() 构造了一个对象,然后在该对象上应用 - 操作符,把 EnumName 对应的值保存到 val 成员中,接着调用 = 操作符, = 操作符啥都不干,从而就相当于删掉了后面的 = SomeValue 部分。
最后,因为 s_value 的元素类型是 Enum,就会调用 operator Enum 把保存的 val 返回去。这个表达式相当于只是在 EnumName = SomeValue 前面增加了一些东西,实现中可以直接使用预定义的 TERARK_PP_PREPEND 宏作为 TERARK_PP_MAP 的 map 函数,其 ctx 就是 prepend 的前缀,即前述的 EnumValueInit() - (注意后面的 - )。
5.6 预处理 & C++:
宏展开仅提供最基本的反射信息,使用模板实现一些包装函数,包装宏展开的反射信息。
使用 inline 函数包装 s_names 与 s_values,有两个理由:
- 针对不同的 Enum 类型,提供重载。
- 保证初始化顺序:不同 translation unit 中的全局对象的初始化顺序是不确定的,如果象 v2 那样,s_name 和 s_value 的初始化顺序与其他 translation unit 中的全局对象初始化顺序不确定,如果在别的 translation unit 中某个全局对象(间接)调用了 Enum Reflection,可能就会导致访问未初始化的 s_name 与 s_value。
另外,利用 C++ 的 parameter dependent name lookup 功能,从而允许 enum 定义在任意的 namespace,甚至可以定义在 class/struct 之内。
enum_rep_type 用来推导 RepType,目前仅用于生成 printf 的 格式化字符串。
当 enum 定义在 class/struct 之内时,宏展开中的 inline 就变成了 friend,这是必要的,否则相关的函数就会变成 enum 外围的那个 class/struct 的成员函数了。
6. 注意事项
如示例代码中的 Value2 = (SomeTemplate<1,2>::value) ,其中的圆括号是必要的,因为预处理器不知道 template 的括号 <> ,不加圆括号会导致宏展开错误,这是混用宏与模板时的一个基本原则。