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

宏二段替换

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

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

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

例如:

#define Def(x)          x
#define Function(x, y)  x ## y
#define Func(x)         #x
#define Fun(x)          ##x      // Not allowed
#define Fn(x)           x#       // Not allowed
#define Defn(x, y)      # x ## y // Not allowed

#define A() 1

Def(A())          // 1
Function(1, A())  // 1A()
Function(A(), 2)  // Error!
Func(A())         // "A()"

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

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

例如:

#define Foo(x) x(x)
#define Bar(x) #x

#define A() Bar

Foo(A()) // "Bar"

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

#define Foo(x)   x(x)
#define Bar(x)   x ## 1()
#define Bar1()  hello

#define A() Bar

Foo(A()) // hello

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

// Example from ISO C

#define F(a) a
#define FUNC(a) (a+1)

/*
* The preprocessor works successively through the input without
* backing up through previous processed preprocessing tokens.
*/
F(FUNC) FUNC (3); // FUNC (3 +1);

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

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

/*
 * detailed:
    expand    F={ FUNC } FUNC={ (3+1) }
    remove    FUNC (3+1)

 * Final tokens output:
    FUNC (3+1)
*/

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

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

蓝染(blue paint)

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

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

一个例子:

// Examples from ISO C

#define A  A B C
#define B  B C A
#define C  C A B

A
/*
 * detailed:
    expand    A={ A B C }
    paint     A={ a B C }
    rescan    A={ a B={ B C A } C }
    paint     A={ a B={ b C a } C }
    rescan    A={ a B={ b C={ C A B } a } C }
    paint     A={ a B={ b C={ c a b } a } C }
    remove    A={ a B={ b c a b a } C }
    remove    A={ a b c a b a C }
    rescan    A={ a b c a b a C={ C A B } }
    paint     A={ a b c a b a C={ c a B } }
    rescan    A={ a b c a b a C={ c a B={ B C A }}}
    paint     A={ a b c a b a C={ c a B={ b c a }}}
    remove    A={ a b c a b a C={ c a b c a }}
    remove    A={ a b c a b a c a b c a }
    remove    a b c a b a c a b c a

 * Final tokens output:
    A B C A B A C A B C A
*/

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

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

再看一个例子:

#define Foo(X) 1 Bar
#define Bar(X) 2 Foo

Foo(X)(Y)(Z)

/*
 * detailed:
    expand    Foo={ 1 Bar }(Y)(Z)
    remove    1 Bar(Y)(Z)
    rescan    1 Bar={ 2 Foo }(Z)
    remove    1 2 Foo(Z)
    rescan    1 2 Foo={ 1 Bar }
    remove    1 2 1 Bar

  * Final tokens output:
    1 2 1 Bar
*/

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

蓝染与递归

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

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

#define RECURSIVE(x) x RECURSIVE(x)

RECURSIVE(1)

/*
 * detailed:
    expand    RECURSIVE={ 1 RECURSIVE(1) }
    paint     RECURSIVE={ 1 recursive(1) }
    remove    1 recursive(1)

 * Final token output:
    1 RECURSIVE(1)
*/

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

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

#define EVAL1(...) __VA_ARGS__
#define RECURSIVE(x) x RECURSIVE(x)

// Not working
EVAL1(RECURSIVE(1))

/*
 * detailed:
    expand    EVAL1( RECURSIVE={ 1 RECURSIVE(1) } )
    paint     EVAL1( RECURSIVE={ 1 recursive(1) } )
    remove    EVAL1( 1 recursive(1) )
    rescan    1 recursive(1)

 * Final token output:
    1 RECURSIVE(1)
*/

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

#define EVAL1(...) __VA_ARGS__
#define RECURSIVE(x) x _RECURSIVE()(x)
#define _RECURSIVE() RECURSIVE

// Not working
EVAL1(RECURSIVE(1))

/*
 * detailed:
    expand    EVAL1( RECURSIVE={ 1 _RECURSIVE()(1) } )
    rescan    EVAL1( RECURSIVE={ 1 RECURSIVE(1) } )
    paint     EVAL1( RECURSIVE={ 1 recursive(1) } )
    remove    EVAL1( 1 recursive(1) )
    rescan    1 recursive(1)

 * Final token output:
    1 RECURSIVE(1)
*/

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

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

#define Bar() 1
Bar() // 1
DEFER(Bar)() // Bar ()

/*
 * DEFER(Bar)()
 * detailed:
    expand    Bar EMPTY()()
    rescan    Bar ()
*/

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

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

#define EMPTY()
#define DEFER(id) id EMPTY()
#define EVAL1(...) __VA_ARGS__
#define RECURSIVE(x) x DEFER(_RECURSIVE) ()(x)
#define _RECURSIVE() RECURSIVE

EVAL1(RECURSIVE(1))

/*
 * detailed:
    expand    EVAL1( RECURSIVE={ 1 DEFER(_RECURSIVE) ()(1) } )
    rescan    EVAL1( RECURSIVE={ 1 _RECURSIVE ()(1) } )
    remove    EVAL1( 1 _RECURSIVE ()(1) )
    rescan    EVAL1={ 1 RECURSIVE(1) ) }
    rescan    EVAL1={ 1 RECURSIVE={1 DEFER(_RECURSIVE) ()(1) } }
    rescan    EVAL1={ 1 RECURSIVE={1 _RECURSIVE ()(1) } }
    remove    EVAL1={ 1 1 _RECURSIVE ()(1) }
    remove    1 1 _RECURSIVE ()(1)

 * Final token output:
    1 1 _RECURSIVE ()(1)
*/

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

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

#define EVAL(...)  EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL2(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))
#define EVAL3(...) EVAL4(EVAL4(EVAL4(__VA_ARGS__)))
#define EVAL4(...) EVAL5(EVAL5(EVAL5(__VA_ARGS__)))
#define EVAL5(...) __VA_ARGS__

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

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

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.