如何优雅地实现 C 编译期静态反射

部门请来了软件专家袁英杰咨询师指导我们软件开发,从中我也学到了很多姿势,在此记录下来宝贵的经验。苹果的 mbp 品控真是差劲,写这个东西把 LShift 键 按坏了,真是难受。


反射能做什么

最近和大师聊软件设计,其中一个点是关于反射,反射最大的作用就是序列化、解序列化一个结构体,然后就能够在各个模块之间进行通信交互,不管是跨进程也好,还是跨机器也好,都缺不了反射这个功能,这也是 OO 世界对象交互的载体。

不然就需要人工手写一堆序列化、反序列代码,不仅代码难看,而且工作量大,容易出错。印象最深的一个例子是,大师在一个电信项目,模块之间通过 TLV 格式的消息进行通信,而这些 TLV 格式也是内部实现的,还不是标准的,然后大师定义了一套机制,只需要统一声明一次元数据的信息,然后通过 include 不同头文件,就能对同一个元数据进行不同的解释,比如序列化、解序列到数据库,序列化、解序列到网络,这也是预编译多态技术,仅用 C 98 的特性就能做到。

举一个直观一点的例子,比如打印一个结构体内容(其实就是把结构体转换成字符串):

struct Point {    double x;    double y;};Point p { 1, 2 };

那么你可能会这样写:

printf("Point x = %d y = %d", p.x, p.y);

如果有成千上百个结构体,对应的打印函数(序列化到字符串)也就成千上百个,如果利用反射手段,只需要写一次,就能给所有反射对象自动生成打印函数(转换)代码。


引子

后来我在 C 社区看到一个讨论,说 C 20 在元编程方面提供了很多便利,其中最大的遍历就是 if-constexpr,再也不用模式匹配写一堆enable_if 了,然后题主给了一个例子,用 C 20 的模板元求结构体的字段数量,代码如下:

struct AnyType {    template <typename T>    operator T();};
template <typename T>consteval size_t CountMember(auto&&... Args) {    if constexpr (! requires { T{ Args... }; }) { // (1)        return sizeof...(Args) - 1;    } else {        return CountMember(Args..., AnyType{}); // (2)    }}
int main(int argc, char** argv) {    struct Test { int a; int b; int c; int d; };    printf("%zu\n", CountMember());}

看到这坨代码,我愣了一会,然后问大师这个求结构体字段数量是怎么做到呢?C 目前最大缺陷是缺少静态反射能力(这里指的是语言层面提供的静态反射信息,C 23估计会落地),应该很难做到的,分析了一会,终于看懂了,太巧妙了:

1. AnyType声明了类型转换操作符(《C Modern design》书中的术语是稻草人函数),可以转换成任意类型。

2. 分支 (2) 通过不断构造所求类型 T = Test,当无法构造时(1),也就是输入的参数过多,这时候参数个数 - 1就是字段个数。

那么只能 C 20 才能做到么?这里主要用到了 C 17 的if-constexpr特性,C 11可以通过 enable-if 做到,而最主要的是那个 requires,C 20 才支持 concept,C 17 都无法做到。

然后我思考了一下,类型构造,《C Modern design》这本书讲过,用 sizeof 做类型推导,给的一个例子是判断一个类是否是另一个类的基类,仅通过 C 98 实现。

C 11 编译期有有两大神器:sizeof   decltype,然后用这两者就能实现同样的功能,这里我用 decltype 来解决上述的 concept 问题:

template <typename T, typename = void, typename ...Ts>struct CountMember {    constexpr static size_t value = sizeof...(Ts) - 1;};
template <typename T, typename ...Ts>struct CountMember, Ts...> {    constexpr static size_t value = CountMembervoid, Ts..., AnyType>::value;};
int main(int argc, char** argv) {    struct Test { int a; int b; int c; int d; };    printf("%zu\n", CountMember::value);}

同样两种情况,用 decltype(T{Ts{}...}) 来判断是否能够构造对象 T。

如何求宏的可变参数个数?

其实这个问题价值不大,而且强依赖平凡构造函数,最大价值在后面的讨论,大师给我出了一道题,如何求宏的可变参数个数?虽然一时半会写不出来,但是之前还是看过一些框架代码的,最终实现方式如下:

#define GET_NTH_ARG(                                                                        \    _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, _28, _29, _30, _31, _32,         \    _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48,         \    _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, n, ...) n
#define GET_ARG_COUNT(...) GET_NTH_ARG(__VA_ARGS__,                     \        64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, \        48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, \        32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, \        16, 15, 14, 13, 12, 11, 10, 9,  8,  7,  6,  5,  4,  3,  2,  1)

GET_ARG_COUNT(a, b, c)展开后,会调用GET_NTH_ARG,然后得到GET_NTH_ARG(a, b, c, 64, 63, ..., 3, 2, 1) 3,从而得到最终长度 3,进一步延伸,这个宏有什么作用呢?那就是对结构体进行反射,用宏提供结构体的元数据信息,从而生成一些类型信息代码。

结合之前看到的那个框架,与大师进一步交流,发现新世界,解决多年来 cpp 静态反射问题,一下子让很多事变成了可能。(后来找到这个实现方法的最早出处:http://pfultz2.com/blog/2012/07/31/reflection-in-under-100-lines/)

来看看大师 actor 框架中的反射例子:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值