《产生式元编程》第三章 替换蓝染概念纤悉

教育   科技   2023-11-26 21:25   美国  

前两章主要集中于应用实践,理论概念都是蜻蜓点水,本章将重点放在这些概念规则上,深入讲解一下。

宏二段替换

源文件扫描后,宏被替换为目标内容,替换实际上分为两个阶段。

第一阶段的替换发生在参数替换之时。

当宏函数含有参数时,该参数会被替换。只有两种情况例外,一是 token 前面含有 ### 预处理标记,二是后面紧跟着 ## 预处理标记。

例如:

 1#define Def(x)          x
2#define Function(x, y)  x ## y
3#define Func(x)         #x
4#define Fun(x)          ##x      // Not allowed
5#define Fn(x)           x#       // Not allowed
6#define Defn(x, y)      # x ## y // Not allowed
7
8#define A() 1
9
10Def(A())          // 1
11Function(1, A())  // 1A()
12Function(A(), 2)  // Error!
13Func(A())         // "A()"

第二阶段的替换发生在重新扫描之时。

此时宏的参数已被第一阶段的替换展开,展开后可能会存在新的宏,为了尽可能多地寻找后续的可替换内容,将再次扫描。

例如:

1#define Foo(x) x(x)
2#define Bar(x) #x
3
4#define A() Bar
5
6Foo(A()) // "Bar"

重新扫描并非只执行一次,而是会一直循环进行,尽可能替换更多内容。比如:

1#define Foo(x)   x(x)
2#define Bar(x)   x ## 1()
3#define Bar1()  hello
4
5#define A() Bar
6
7Foo(A()) // hello

需要注意,预处理器按顺序处理输入,对于之前处理过的 token,不会再作替换。例如:

 1// Example from ISO C
2
3#define F(a) a
4#define FUNC(a) (a+1)
5
6/*
7* The preprocessor works successively through the input without
8* backing up through previous processed preprocessing tokens.
9*/

10F(FUNC) FUNC (3); // FUNC (3 +1);

此处,替换宏函数的参数列表,将 F(FUNC) 替换成 FUNC, FUNC (3) 替换成 (3+1)。由于 FUNC 已经被处理过,后续不会发生重新扫描,展开成 FUNC (3+1); 便停止处理。

具体处理步骤可表示如下:

1/*
2 * detailed:
3    expand    F={ FUNC } FUNC={ (3+1) }
4    remove    FUNC (3+1)
5
6 * Final tokens output:
7    FUNC (3+1)
8*/

若非函数宏,或无参宏函数,则不会发生参数替换,直接从扫描替换开始。通过前面章节的学习,大家清楚,这种循环进行的扫描替换,并非在任何时候都会进行下去。

什么时候会停止呢?便是蓝染时刻。

蓝染(blue paint)

blue paint 是 C 预处理器中的一个黑话,一次扫描中,若是替换的 token 引用了其自身,该 token 就会被标记为不可处理状态。这个标记动作就称为 Painted blue

名称缘由已不可考,据说是由 C 工程师标记 token 的墨水颜色而来,便延续着叫了。蓝染是本文为书写方便所采取的翻译,该词原为中国古代印染工艺的术语。

一个例子:

 1// Examples from ISO C
2
3#define A  A B C
4#define B  B C A
5#define C  C A B
6
7A
8/*
9 * detailed:
10    expand    A={ A B C }
11    paint     A={ a B C }
12    rescan    A={ a B={ B C A } C }
13    paint     A={ a B={ b C a } C }
14    rescan    A={ a B={ b C={ C A B } a } C }
15    paint     A={ a B={ b C={ c a b } a } C }
16    remove    A={ a B={ b c a b a } C }
17    remove    A={ a b c a b a C }
18    rescan    A={ a b c a b a C={ C A B } }
19    paint     A={ a b c a b a C={ c a B } }
20    rescan    A={ a b c a b a C={ c a B={ B C A }}}
21    paint     A={ a b c a b a C={ c a B={ b c a }}}
22    remove    A={ a b c a b a C={ c a b c a }}
23    remove    A={ a b c a b a c a b c a }
24    remove    a b c a b a c a b c a
25
26 * Final tokens output:
27    A B C A B A C A B C A
28*/

示例中,使用小写字母表示蓝染标记的 token,一次扫描实际指的是一个 token 的一次扫描,并非是说不会发生重新扫描。

宏预处理器按顺序逐个处理 token,展开、替换、蓝染、移除…… 因为ABC相互引用,当所有 token 都被处理完成之后,它们也全部被蓝染标记。

再看一个例子:

 1#define Foo(X) 1 Bar
2#define Bar(X) 2 Foo
3
4Foo(X)(Y)(Z)
5
6/*
7 * detailed:
8    expand    Foo={ 1 Bar }(Y)(Z)
9    remove    1 Bar(Y)(Z)
10    rescan    1 Bar={ 2 Foo }(Z)
11    remove    1 2 Foo(Z)
12    rescan    1 2 Foo={ 1 Bar }
13    remove    1 2 1 Bar
14
15
16  * Final tokens output:
17    1 2 1 Bar
18*/

此处,虽为宏函数,但参数却形同虚设,没有发生参数替换,也没有发生蓝染标记,就是不断扫描、替换、再扫描…… 重复进行,直到无可替换。

蓝染与递归

蓝染是 C 预处理器不支持递归的原因,知道了这个原理,再去看第二章的内容,便能够剖析入微,鞭辟入里。

这里摘出来一个简洁版本讲解:

 1#define RECURSIVE(x) x RECURSIVE(x)
2
3RECURSIVE(1)
4
5/*
6 * detailed:
7    expand    RECURSIVE={ 1 RECURSIVE(1) }
8    paint     RECURSIVE={ 1 recursive(1) }
9    remove    1 recursive(1)
10
11 * Final token output:
12    1 RECURSIVE(1)
13*/

RECURSIVE(1) 处理完成之后被蓝染,致递归未始即终。

蓝染后的 token,即便强制展开,也无半点效果。

 1#define EVAL1(...) __VA_ARGS__
2#define RECURSIVE(x) x RECURSIVE(x)
3
4// Not working
5EVAL1(RECURSIVE(1))
6
7/*
8 * detailed:
9    expand    EVAL1( RECURSIVE={ 1 RECURSIVE(1) } )
10    paint     EVAL1( RECURSIVE={ 1 recursive(1) } )
11    remove    EVAL1( 1 recursive(1) )
12    rescan    1 recursive(1)
13
14 * Final token output:
15    1 RECURSIVE(1)
16*/

因为重新扫描会循环发生,所以即使你增加一些间接层,也不会对结果产生丝毫影响。

 1#define EVAL1(...) __VA_ARGS__
2#define RECURSIVE(x) x _RECURSIVE()(x)
3#define _RECURSIVE() RECURSIVE
4
5// Not working
6EVAL1(RECURSIVE(1))
7
8/*
9 * detailed:
10    expand    EVAL1( RECURSIVE={ 1 _RECURSIVE()(1) } )
11    rescan    EVAL1( RECURSIVE={ 1 RECURSIVE(1) } )
12    paint     EVAL1( RECURSIVE={ 1 recursive(1) } )
13    remove    EVAL1( 1 recursive(1) )
14    rescan    1 recursive(1)
15
16 * Final token output:
17    1 RECURSIVE(1)
18*/

因此,需要一种方法,能够禁止重新扫描。这种方法就是延迟扫描,基本代码逻辑如下所示:

 1#define EMPTY()
2#define DEFER(id) id EMPTY()
3
4#define Bar() 1
5Bar() // 1
6DEFER(Bar)() // Bar ()
7
8/*
9 * DEFER(Bar)()
10 * detailed:
11    expand    Bar EMPTY()()
12    rescan    Bar ()
13*/

第一节说过,预处理器按顺序处理输入,对于之前处理过的 token,不会再作处理。Bar 便是已经处理过的 token,是以不会再对它扫描。

将这个技术应用到递归代码中,便可以避免蓝染标记。

 1#define EMPTY()
2#define DEFER(id) id EMPTY()
3#define EVAL1(...) __VA_ARGS__
4#define RECURSIVE(x) x DEFER(_RECURSIVE) ()(x)
5#define _RECURSIVE() RECURSIVE
6
7EVAL1(RECURSIVE(1))
8
9/*
10 * detailed:
11    expand    EVAL1( RECURSIVE={ 1 DEFER(_RECURSIVE) ()(1) } )
12    rescan    EVAL1( RECURSIVE={ 1 _RECURSIVE ()(1) } )
13    remove    EVAL1( 1 _RECURSIVE ()(1) )
14    rescan    EVAL1={ 1 RECURSIVE(1) ) }
15    rescan    EVAL1={ 1 RECURSIVE={1 DEFER(_RECURSIVE) ()(1) } }
16    rescan    EVAL1={ 1 RECURSIVE={1 _RECURSIVE ()(1) } }
17    remove    EVAL1={ 1 1 _RECURSIVE ()(1) }
18    remove    1 1 _RECURSIVE ()(1)
19
20 * Final token output:
21    1 1 _RECURSIVE ()(1)
22*/

第二节说过,蓝染标记只在一次扫描期间存在,通过间接性加延迟扫描,RECURSIVE 便永远不会再被蓝染标记。但是,禁用重新扫描,我们也会因此失去循环扫描的能力,必须手动通过 EVAL1 来运行一次扫描。

手动调用扫描的次数和递归的深度成正比,一次次手动太过麻烦,故而一般会提前定义好多层扫描,以便后续使用。

1#define EVAL(...)  EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
2#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
3#define EVAL2(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))
4#define EVAL3(...) EVAL4(EVAL4(EVAL4(__VA_ARGS__)))
5#define EVAL4(...) EVAL5(EVAL5(EVAL5(__VA_ARGS__)))
6#define EVAL5(...) __VA_ARGS__

有了该工具,便可基本模拟宏递归,本章只是穿插着讲原理,更多使用见第二章。


总的来说,宏预处理器的替换规则算不上晦涩,但相关资料寥寥无几,文章更是少得可怜,像是黑盒子,导致理解起来并不容易。本章对此进行了深入剖析,篇幅不长,主要是应用放在了第二章,这部分有一定难度,仍归为四星。


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