C++ Enum Reflection

1. 概述

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

2. 用法

2.2 宏定义

3. 支持的功能

3.1 函数

支持的函数都定义在全局 namespace 中:

3.2 举例说明:

上面的宏展开会生成以下代码:

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  的功能,可以利用操作符重载来实现:

这样,有了 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,有两个理由:

  1. 针对不同的 Enum 类型,提供重载。
  2. 保证初始化顺序:不同 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 的括号 <> ,不加圆括号会导致宏展开错误,这是混用宏与模板时的一个基本原则。

作者:
该日志由 rockeet 于2019年12月23日发表在C++分类下, 你可以发表评论,并在保留原文地址及作者的情况下引用到你的网站或博客。
转载请注明: C++ Enum Reflection
标签:
【上一篇】

您可能感兴趣的文章:

发表评论

您必须 登录 后才能发表评论。