简而言之,编程语言中的反射(Reflection)指的是从运行时中获取语言本身的类型等信息。C++ 缺乏这样的机制,对于最简单的 enum 类型,我们或许可以实现带有反射功能的 enum。 我们实现了几个宏,通过宏定义的 enum,就自动地拥有反射功能。
// 可在任意 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...
支持的函数都定义在全局 namespace 中:
template<class Enum> Slice enum_name(Enum v);
template<class Enum> bool enum_value(const Slice& name, Enum* result);
/// for convenient
template<class Enum> Enum enum_value(const Slice& name, Enum Default);
// use case:
// enum_for_each([](Slice name, Enum val){...});
template<class Enum> void enum_for_each(Func fn);
template<class Enum> std::string enum_str_all_names();
#include <rocksdb/enum_reflection.hpp>
// 在 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<const Slice*, size_t>
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;
}
最典型的应用场景莫过于处理配置信息,把用户配置的字符串,转化为 Enum 值,写 Log 时,又把 Enum 转化为字符串。例如 RocksDB 中就有大量此类场景。
目前,该 enum reflection 已经向 RocksDB 提交为 Pull Request,用来改善 RocksDB 中大量手工实现的 enum reflection(样例)。
为了突出重点,仅说明实现中的几个关键点。
s_name
与 s_value
是平行数组,name
为 s_name[i]
的 enum,其值为 s_value[i]
。这两个平行数组几乎可以用来实现所有的反射功能,它们分别在 enum_all_name
s 和 enum_all_values
中定义。 关键点是通过宏展开如何生成 s_name
与 s_value
。
遍历该宏的变参列表,生成一个结果列表,该宏的实现包含了一点奇技淫巧,但限制变参列表长度最大为 61(Visual C++ 最多支持 127 个宏参数,gcc 支持近乎无限个宏参数)。
EnumName = SomeValue
这样的语法结构,作为宏参数时,它是一个整体,可以把它变成一个字符串 "EnumName = SomeValue"
,除此之外,无法对它进行其它操作(我们期望的拆解)。
作为 enum 的 name,在 EnumName = SomeValue
中,我们只需要 EnumName,这个比较容易处理,我们实现了一个 var_symbol 函数,可以从中把 EnumName 切分出来。 在 s_name 的初始化列表中,我们利用 ROCKSDB_PP_MAP
,逐个调用 var_symbol 函数,生成 EnumName。所以,相比 s_value 的初始化,s_name 的初始化是比较简单的。
s_value 的初始化中也要处理 EnumName = SomeValue
,因为要获取 EnumName 的值,而不是其字符串形式,我们要处理的就是 EnumName = SomeValue
的整个语法结构,其中 = SomeValue
是可选的,所以我们应该只保留 EnumName
,而删去 = SomeValue
,这个需求在预处理器中无法完成。
我们就只有想办法利用 C++ 的语法,实现 删去 = SomeValue
的功能,可以利用操作符重载来实现:
template<class Enum>
class EnumValueInit {
Enum val;
public:
operator Enum() const { return val; }
EnumValueInit& operator-(Enum v) { val = v; return *this; }
template<class IntRep> /// 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() -
(注意后面的 -
)。
宏展开仅提供最基本的反射信息,使用模板实现一些包装函数,包装宏展开的反射信息。
使用 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 的成员函数了。
如示例代码中的 Value2 = (SomeTemplate<1,2>::value)
,其中的圆括号是必要的,因为预处理器不知道 template 的括号 <>
,不加圆括号会导致宏展开错误,这是混用宏与模板时的一个基本原则。