1. 概述
简而言之,编程语言中的反射(Reflection)指的是从运行时中获取语言本身的类型等信息。C++ 缺乏这样的机制,对于最简单的 enum 类型,我们或许可以实现带有反射功能的 enum。 我们实现了几个宏,通过宏定义的 enum,就自动地拥有反射功能。
2. 用法
2.2 宏定义
// 可在任意 namespace 中调用,不可在 struct/class 内调用
#define ROCKSDB_ENUM_PLAIN(EnumType, IntRep, ...) details...
#define ROCKSDB_ENUM_CLASS(EnumType, IntRep, ...) details...
// 可在 struct/class 内调用,不可在任意 namespace 中调用
#define ROCKSDB_ENUM_PLAIN_INCLASS(EnumType, IntRep, ...) details...
#define ROCKSDB_ENUM_CLASS_INCLASS(EnumType, IntRep, ...) details...3. 支持的功能
3.1 函数
支持的函数都定义在全局 namespace 中:
template Slice enum_name(Enum v);
template bool enum_value(const Slice& name, Enum* result);
/// for convenient
template Enum enum_value(const Slice& name, Enum Default);
// use case:
// enum_for_each([](Slice name, Enum val){...});
template void enum_for_each(Func fn);
template std::string enum_str_all_names(); 3.2 举例说明:
#include
// 在 namespace 中调用该宏,不能在 class/struct 内调用
ROCKSDB_ENUM_CLASS(MyEnum, char,
Value1,
Value2 = (SomeTemplate<1,2>::value),
Value3 = 30 // 限制:这里不能有逗号
) 上面的宏展开会生成以下代码:
// 宏展开的 enum 定义
enum class MyEnum : int {
Value1, Value2 = (SomeTemplate<1,2>::value), Value3 = 30
};
// 宏展开的反射功能:
int enum_rep_type(MyEnum*);
inline Slice enum_str_define(MyEnum*) {
return "enum class MyEnum : int"
" { Value1, Value2 = (SomeTemplate<1,2>::value), Value3 = 30 }";
}
inline std::pair
enum_all_names(MyEnum*) {
static const Slice s_names[] = {
var_symbol("Value1"),
var_symbol("Value2 = (SomeTemplate<1,2>::value)"),
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 宏 ROCKSDB_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 的初始化列表中,我们利用
ROCKSDB_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 的功能,可以利用操作符重载来实现:
template
class EnumValueInit {
Enum val;
public:
operator Enum() const { return val; }
EnumValueInit& operator-(Enum v) { val = v; return *this; }
template /// absorb the IntRep param
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 前面增加了一些东西,实现中可以直接使用预定义的
ROCKSDB_PP_PREPEND 宏作为
ROCKSDB_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 的括号
<> ,不加圆括号会导致宏展开错误,这是混用宏与模板时的一个基本原则。