《产生式元编程》第二章 自复用代码生成技

教育   科技   2023-09-30 22:26   美国  

衔引

原理毕,复用续。

历观编程概念,避及重复,提其不变,以迭代与递归为主要技术。产生式元编程,乃欲自动产生百千代码,迭代递归自是重要组件,不可不备。

本章以 FOR_EACH 为例,渐次派生问题,引出技术要点,实现强大组件。

迭代

FOR_EACH 用来迭代数据集合,依次取出数据,后调用函数处理。参数易定,一个回调函数加上可变参数。代码表示:

#define FOR_EACH(call, ...)

// Invoke
FOR_EACH(Foo, 123)

实现需遂个取出可变宏参数,调用 Foo() 处理。上章已讲解重载之实现法,此乃条件逻辑在宏中的表示法。无循环时,递归思想便是拆解参数包的唯一方式,将重载实现法与递归思想结合起来,即可得遂个取参数之效。

遂可如此表示:

#define _FOR_EACH_1(call, x) call(x)
#define _FOR_EACH_2(call, x, ...) call(x) _FOR_EACH_1(call, __VA_ARGS__)
#define _FOR_EACH_3(call, x, ...) call(x) _FOR_EACH_2(call, __VA_ARGS__)
#define _FOR_EACH_4(call, x, ...) call(x) _FOR_EACH_3(call, __VA_ARGS__)
#define _FOR_EACH_5(call, x, ...) call(x) _FOR_EACH_4(call, __VA_ARGS__)

#define FOR_EACH(call, ...) \
    OVERLOAD_INVOKE(_FOR_EACH, COUNT_VARARGS(__VA_ARGS__))(call, __VA_ARGS__)

OVERLOAD_INVOKE() 在上章的基础上已部分优化,如今的实现变为:

#define CONCAT_HELPER(a, b) a ## b
#define CONCAT(a, b) CONCAT_HELPER(a, b)

#define OVERLOAD_INVOKE_1(call, v1) CONCAT(CONCAT(call, _), v1)
#define OVERLOAD_INVOKE_2(call, v1, v2) CONCAT( CONCAT(OVERLOAD_INVOKE_1(call, v1), _), v2 )
#define OVERLOAD_INVOKE_3(call, v1, v2, v3) CONCAT( CONCAT(OVERLOAD_INVOKE_2(call, v1, v2), _), v3)
#define OVERLOAD_INVOKE_4(call, v1, v2, v3, v4) CONCAT( CONCAT(OVERLOAD_INVOKE_3(call, v1, v2, v3), _), v4)
#define OVERLOAD_INVOKE_5(call, v1, v2, v3, v4, v5) CONCAT( CONCAT(OVERLOAD_INVOKE_4(call, v1, v2, v3, v4), _), v5)
#define OVERLOAD_INVOKE(call, ...) CONCAT( OVERLOAD_INVOKE_, COUNT_VARARGS(__VA_ARGS__) )(call, __VA_ARGS__)

因其使用地方太过广泛,太过频繁,于是提前重新封装成更适用的函数。它基于 COUNT_VARARGS() 实现,而在上章 COUNT_VARARGS() 又依赖于 OVERLOAD_INVOKE(),造成循环依赖。

为解决此问题,将原本的重载调用功能,抽象为 CONCAT() 函数,如此一来,COUNT_VARARGS()OVERLOAD_INVOKE() 都可以基于 CONCAT() 实现。

宏函数并非真的支持重载,而是以分发调用模拟重载。分发函数可能有很多主次版本,为 OVERLOAD_INVOKE() 定义多个版本的实现,更易使用。

FOR_EACH() 就是以 OVERLOAD_INVOKE() 来调用不同的重载函数,不同的重载函数之间又利用递归思维,复用已有实现,以达到预想效果。

对于空包,在实现 COUNT_VARARGS() 时是个大问题,但此处并不会实际使用该参数,对结果没有任何影响,故不是问题。只需如此实现便可:

#define _FOR_EACH_0(call, ...)

更进一步,可以像第一章那样,以强制展开来让实现可以跨平台,此处省略。

使用例子:

void foo(int x) {
    printf("x: %d\n", x);
}

#define Foo(x) foo(x);
FOR_EACH(Foo, 012)

/**
 * Output:
 * x: 1
 * x: 2
 * x: 3
*/

这里也是为 foo() 的传递提供了一个中间件 Foo()。由于 FOR_EACH() 的实现中call(x) 后面并没有添加 ;,直接传递 foo 难以增加该 ;

中间件能够延迟生成,为处理提供缓冲时间。传递之时名称为 Foo,而非 Foo(arg),仅是一个普通名称,直到将 call(x) 替换为 Foo(x) 时,才真正开始生成代码,最终替换为 foo(x);

当然也可以在实现中直接写成 call(x);,但会降低 FOR_EACH() 的通用性。

序列

调用 FOR_EACH(Foo, 0, 1, 2) 时,其中包含序列,相当于 C++ 中的 std::index_sequence<0, 1, 2>,数量较少时还可以手动写,数量较多时则异常麻烦。

C++ 通过 std::make_index_sequence<N> 来自动生成序列,简化了写法。我们也来提供一个宏版本的 MAKE_INDEX_SEQUENCE(N), 自动生成序列。

同样利用重载加递归思想,很轻松地就能实现该需求。

#define MAKE_INDEX_SEQUENCE_0() 0
#define MAKE_INDEX_SEQUENCE_1() MAKE_INDEX_SEQUENCE_0(), 1
#define MAKE_INDEX_SEQUENCE_2() MAKE_INDEX_SEQUENCE_1(), 2
#define MAKE_INDEX_SEQUENCE_3() MAKE_INDEX_SEQUENCE_2(), 3
#define MAKE_INDEX_SEQUENCE_4() MAKE_INDEX_SEQUENCE_3(), 4
#define MAKE_INDEX_SEQUENCE_5() MAKE_INDEX_SEQUENCE_4()
#define MAKE_INDEX_SEQUENCE(index) OVERLOAD_INVOKE(MAKE_INDEX_SEQUENCE, DEC(index))()

举个例子,调用 MAKE_INDEX_SEQUENCE(5),将生成 0, 1, 2, 3, 4。实现过程中,需要调用次一版本的重载,5 需要减少一,代码中另外声明了 DEC() 函数,用于实现数字自减。

DEC() 的实现原理也是重载,没有难度。

// DEC(Decrement by 1)
#define DEC_1() 0
#define DEC_2() 1
#define DEC_3() 2
#define DEC_4() 3
#define DEC_5() 4
#define DEC_6() 5
#define DEC_7() 6
#define DEC_8() 7
#define DEC_9() 8
#define DEC_10() 9
#define DEC(value) OVERLOAD_INVOKE(DEC, value)()

同理可以实现自增运算。

// INC(Increment by 1)
#define INC_0() 1
#define INC_1() 2
#define INC_2() 3
#define INC_3() 4
#define INC_4() 5
#define INC_5() 6
#define INC_6() 7
#define INC_7() 8
#define INC_8() 9
#define INC_9() 10
#define INC(value) OVERLOAD_INVOKE(INC, value)()

虽有 MAKE_INDEX_SEQUENCE(N) 生成序列,但却无法指定区间,只能生成 [0, N) 的序列。若想生成 [a, b) 的序列,如何实现呢?

判等

问题的关键在于结束条件的判断,满足结束条件立即停止生成,否则继续生成。

先写出区间函数原型,逐步实现。

#define RANGE(begin, end) OVERLOAD_INVOKE(RANGE, begin)(end)
#define RANGE_0(end) 0 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(0, DEC(end)))(1, end)
#define RANGE_1(end) 1 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(1, DEC(end)))(2, end)
#define RANGE_2(end) 2 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(2, DEC(end)))(3, end)

宏只有替换这一个功能,一切需求皆得回归原理思考。我们需要条件判断,而已有重载函数这把称手兵器,这并非问题。

两个地方需要用到条件。一是当前索引是否递归到末尾,如 [a = 0, b = 2)aRANGE_0() 开始生成,若是当前索引 a == DEC(b),则返回 1,否返回 0。二是根据条件一返回的结果,分发处理,1 则停止生成,2 则继续生成下一个数字。

条件一通过 IS_EQUAL_INT(a, DEC(end)) 实现,初步代码为:

#define IS_EQUAL_INT_0_0() 1
#define IS_EQUAL_INT_1_1() 1
#define IS_EQUAL_INT_2_2() 1
#define IS_EQUAL_INT_3_3() 1
#define IS_EQUAL_INT_4_4() 1
#define IS_EQUAL_INT_5_5() 1
#define IS_EQUAL_INT_6_6() 1
// ...

#define IS_EQUAL_INT(a, b) OVERLOAD_INVOKE(IS_EQUAL_INT, a, b)()

对于相等的情况,通过重载非常好写,问题在于,如何表示不相等的情况?每个数字都要比较,穷举法自然无法穷尽,此时须得另辟蹊径。

根据第一章的结论 1.1,对于无法解决的问题,能够通过增加一个间接层来解决。是以考虑把结果再单独添加一个 IS_EQUAL_INT_RESULT() 表示,结果非 0 即 1。

如何产生非 0 即 1 的效果?在第一章中,以实现 COUNT_VARARGS() 展示了无数宏原理和技术,技术虽多,只取一瓢即够。方式就是当时所用的以 , 将空包变为多参的技巧,我们只需使得 IS_EQUAL_INT_X_X() 返回的参数个数不同,再借助 COUNT_VARARGS() 来分发结果便能达到目的。

怎么让返回的参数个数不等?, 便有此妙用。于是实现变为:

#define IS_EQUAL_INT_0_0() ,
#define IS_EQUAL_INT_1_1() ,
#define IS_EQUAL_INT_2_2() ,
#define IS_EQUAL_INT_3_3() ,
#define IS_EQUAL_INT_4_4() ,
#define IS_EQUAL_INT_5_5() ,
#define IS_EQUAL_INT_6_6() ,
// ...

#define IS_EQUAL_INT_RESULT_2() 1
#define IS_EQUAL_INT_RESULT_1() 0
#define IS_EQUAL_INT_RESULT(...) OVERLOAD_INVOKE(IS_EQUAL_INT_RESULT, COUNT_VARARGS(__VA_ARGS__))()
#define IS_EQUAL_INT(a, b) IS_EQUAL_INT_RESULT( OVERLOAD_INVOKE(IS_EQUAL_INT, a, b)() )

条件一目的达成。

死路

条件二通过 RANGE_GEN_WHEN_X() 实现,根据 IS_EQUAL_INT() 返回的结果调用不同的版本。

代码为:

#define RANGE_GEN_WHEN_1(cur, end)
#define RANGE_GEN_WHEN_0(cur, end) , OVERLOAD_INVOKE(RANGE, cur)(end)

#define RANGE(begin, end) OVERLOAD_INVOKE(RANGE, begin)(end)
#define RANGE_0(end) 0 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(0, DEC(end)))(1, end)
#define RANGE_1(end) 1 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(1, DEC(end)))(2, end)
#define RANGE_2(end) 2 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(2, DEC(end)))(3, end)
#define RANGE_3(end) 3 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(3, DEC(end)))(4, end)

当返回结果为 1 时,表示序列生成结束,什么也不必做;当结果为 0 时,表示需要接着生成序列,于是再次调用 RANGE_X 的新版本。循此往复。

测试一下:

0 , 1 RANGE_GEN_WHEN_0(23)

想法很完美,现实很骨感。展开成了:

0 , 1 RANGE_GEN_WHEN_0(23)

于是遂步分析一下,检查是否存在问题。

RANGE(03) ->
1. OVERLOAD_INVOKE(RANGE, 0)(3)
2. RANGE_0(3)
3. 0 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(0, DEC(3)))(13)
4. 0 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(02))(13)
5. 0 OVERLOAD_INVOKE(RANGE_GEN_WHEN, 0)(13)
6. 0 RANGE_GEN_WHEN_0(13)
7. 0 , OVERLOAD_INVOKE(RANGE, 1)(3)
8. 0 , RANGE_1(3)
9. 0 , 1 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(1, DEC(3)))(23)
10. 0 , 1 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(12))(23)
11. 0 , 1 OVERLOAD_INVOKE(RANGE_GEN_WHEN, 0)(23)
12. 0 , 1 RANGE_GEN_WHEN_0(23)

如今停在第 12 步,换成 RANGE(0, 1)RANGE(0, 2)RANGE(1, 2)RANGE(1, 3) 得到却的是正确的结果。也就是说,调用层次不能多于二层,二层之后若是调用 RANGE_GEN_WHEN_1(),能够得到正确的结果;若是调用 RANGE_GEN_WHEN_0() 便无法进一步展开。

这其实并非是思路的问题,而是宏自身的问题。宏并非是一套图灵完备的系统,并不支持真正的递归。而二层以上,RANGE_GEN_WHEN_0() 又再次调用了自身,此时不会进一步展开。

这条路走死了。

递归

为了深入分析死路原因,我们将实现进一步简化,去除多个重载版本,直接基于 RANGE() 递归。

#define RANGE_GEN_WHEN_1(cur, end)
#define RANGE_GEN_WHEN_0(cur, end) , RANGE(cur, end)

#define RANGE(begin, end) begin OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(begin, end)) (INC(begin), end)

代码思路依旧未变,只不过前面一直采用多个函数重载来模拟递归,所以一直没有出现问题,这里直接使用递归。如果能将其实现,则前面的很多功能实现都可得到极大简化。

依旧调用 RANGE(0, 3),展开结果为:

0 , RANGE(13)

过程和之前相同,只是递归调用提前,替换也就提前停止。

RANGE(03) ->
1. 0 OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(03)) (INC(0), 3)
2. 0 OVERLOAD_INVOKE(RANGE_GEN_WHEN, 0) (13)
3. 0 RANGE_GEN_WHEN_0 (13)
4. 0 , RANGE(13)

术语上来说,RANGE() 扫描展开后,便会创建一个禁用上下文(disabling context),禁用上下文会导致引用当前扩展宏的 token 被标记为 painted blue,任何方式都无法再展开。

不过禁用上下文只在一次扫描期间存在,因此可以避免引用当前扩展宏,将它延迟到下一轮扫描期间,便可以解决这个问题。延迟操作同样是通过增加间接性来实现。

#define EMPTY()
#define DEFER(id) id EMPTY()

#define RANGE_GEN_WHEN_1(cur, end)
#define RANGE_GEN_WHEN_0(cur, end) , DEFER(RANGE_INDIRECT)()(cur, end)

#define RANGE_INDIRECT() RANGE
#define RANGE(begin, end) begin OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(begin, end)) (INC(begin), end)

DEFER() 的作用就是延迟宏展开。例如:

#define Bar() 1
Bar() --> 1
DEFER(Bar)() --> Bar()

由于延迟展开,Bar 在当前没有彻底展开成 1。

于是,以 DEFER() 为工具,加用 RANGE_INDIRECT()RANGE() 增加一层间接性,最后生成的结果为:

RANGE(03) ->
0 , RANGE_INDIRECT ()(13)

这里 RANGE 不会再被标记为 painted blue,由此可以开启新一轮的扫描,这个操作可以通过强制展开来完成。

#define EXPAND(...) __VA_ARGS__

EXPAND( RANGE(03) ) ->
0 , 1 , RANGE_INDIRECT ()(23)

EXPAND(EXPAND( RANGE(03) )) ->
0 , 1 , 2 , RANGE_INDIRECT ()(33)

EXPAND(EXPAND(EXPAND( RANGE(03) ))) ->
0 , 1 , 2 , 3

每次手动调用新一轮的扫描较为麻烦,可以预先定义多轮扫描。

#define EXPAND(...)  EXPAND1(EXPAND1(EXPAND1(__VA_ARGS__)))
#define EXPAND1(...) EXPAND2(EXPAND2(EXPAND2(__VA_ARGS__)))
#define EXPAND2(...) EXPAND3(EXPAND3(EXPAND3(__VA_ARGS__)))
#define EXPAND3(...) EXPAND4(EXPAND4(EXPAND4(__VA_ARGS__)))
#define EXPAND4(...) EXPAND5(EXPAND5(EXPAND5(__VA_ARGS__)))
#define EXPAND5(...) __VA_ARGS__

于是调用可以得到预想结果。

EXPAND( RANGE(03) ) ->
0 , 1 , 2 , 3

这便是宏递归的实现之法。

应用

可以再把 RANGE() 封装一下,简化使用接口。

#define RANGE_IMPL(begin, end) begin OVERLOAD_INVOKE(RANGE_GEN_WHEN, IS_EQUAL_INT(begin, end)) (INC(begin), end)
#define RANGE(begin, end) EXPAND( RANGE_IMPL(begin, end) )

#define RANGE_INDIRECT() RANGE_IMPL

配合 FOR_EACH() 使用,可以批量生成序列变量,比如:

#define DECLARE_VARIABLES(var) int variable_ ## var;
FOR_EACH(DECLARE_VARIABLES, RANGE(25))

将自动生成如下代码:

int variable_2; int variable_3; int variable_4; int variable_5;

同样也可以批量生成类,如:

#define DECLARE_CLASS(cls) class cls{};
FOR_EACH(DECLARE_CLASS, Bar, Duck)

生成如下代码:

class Foo{}; class Bar{};

再比如,批量生成命名空间:

#define START_NS(ns) namespace ns {
#define END_NS(ns) }
#define MY_NAMESPACE System, Net, Http
FOR_EACH(START_NS, MY_NAMESPACE)
int X = 42;
FOR_EACH(END_NS, MY_NAMESPACE)

生成如下代码:

namespace System { namespace Net { namespace Http {
int X = 42;
} } }

借助这些工具,自动生成成百上千行代码,亦不在话下。

总结

本章以宏的核心原理进行扩展,开发 FOR_EACH 工具,迭代与递归技术穿插其中,再辅以很多小工具的实现手法,最终实现强大的代码生成组件。

灵活技巧,几近涵盖,巧妙手法,未有藏漏。

还请大家多多支持本系列,在看、点赞、分享…… 后续章节更加精彩。


CppMore
Dive deep into the C++ core, and discover more!
 最新文章