Enum-Reflection

1. 概述

简而言之,编程语言中的反射(Reflection)指的是从运行时中获取语言本身的类型等信息。C++ 缺乏这样的机制,对于最简单的 enum 类型,我们或许可以实现带有反射功能的 enum。
我们实现了几个宏,通过宏定义的 enum,就自动地拥有反射功能。

2. 用法

2.2 宏定义

1
2
3
4
5
6
7
// 可在任意 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 中:

1
2
3
4
5
6
7
8
9
10
11
12
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();

3.2 举例说明:

1
2
3
4
5
6
7
#include <rocksdb/enum_reflection.hpp>
// 在 namespace 中调用该宏,不能在 class/struct 内调用
ROCKSDB_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 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;
}

4. 应用场景

最典型的应用场景莫过于处理配置信息,把用户配置的字符串,转化为 Enum 值,写 Log 时,又把 Enum 转化为字符串。例如 RocksDB 中就有大量此类场景。

目前,该 enum reflection 已经向 RocksDB 提交为 Pull Request,用来改善 RocksDB 中大量手工实现的 enum reflection(样例)

5. 实现细节

为了突出重点,仅说明实现中的几个关键点。

5.1 s_name 与 s_value

s_names_value 是平行数组,names_name[i] 的 enum,其值为 s_value[i]。这两个平行数组几乎可以用来实现所有的反射功能,它们分别在 enum_all_names 和 enum_all_values 中定义。
关键点是通过宏展开如何生成 s_names_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 的功能,可以利用操作符重载来实现:

1
2
3
4
5
6
7
8
9
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。这个表达式就是:

1
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 的括号 <> ,不加圆括号会导致宏展开错误,这是混用宏与模板时的一个基本原则。