这里讨论的C语言宏系统仅关于#define,不包含#include, #if,#ifdef,#elif,#else,#endif等。
C语言宏的可选参数
前段时间在某个嵌入式软件项目中涉及一个这样的功能,需要将一个wav音频文件嵌入式到二进制固件中,这样可以方便获取音频文件数据且不需要文件系统等复杂的组件。以前的做法是将音频文件写入到Flash中固定的地址下,然后在软件代码中从该地址访问相关的数据。但是这样的做法存在一定的灵活性问题,比如文件是否可能和固件重叠、无法统一烧录等问题。
后面我想到可以使用gcc汇编中的一条命令来实现在代码中包含一个文件,那就是.incbin指令。该指令在手册的描述如下:
1 | .incbin "file"[,skip[,count]] |
将这个命令简单的使用宏封装一下,就可以在C代码文件中非常轻松的嵌入一个音频文件了。
1 |
封装之后,就可以在C代码中直接使用如下方式来嵌入音频文件:
1 | INCFILE(audio, "audio.wav"); |
在后来的使用过程中发现wav音频文件前的44字节是音频文件头,可以忽略,只需要使用后面的PCM数据即可。这样需要使用incbin指令的skip选项,但是上面涉及的INCFILE()宏没有提供这个功能,所以需要重新写一个宏。
1 |
这样的写法似乎也不太符合要求,因为这里不需要count参数,而是需要包含文件的全部内容,所以需要再封装一个宏?
类似的操作为什么需要写多个宏?这样的操作似乎就太不优雅了。
能不能实现一个这样的宏,能够提供多个可选参数呢?根据参数个数生成不同的代码。
1 | INCFILE(audio, "audio.wav") |
这样就引入了一个非常关键的问题,C语言的宏定义如何根据参数个数来动态生成代码呢?
1 |
可以很明确的说,上面的代码是错误的,C语言的宏定义无法执行条件判断,所以无法实现这个功能。
宏定义如何实现条件分支流程呢?我们从一个简单的需求开始,请设计一个宏,判断宏参数列表中的参数个数是0个还是1个。
1 |
我们来看看这段宏填入一些参数后展开的情况:
1 | /* |
注意力惊人的人已经注意到展开后的结果中第三个数据就是该宏输入的参数个数。这似乎就解决了参数条件判断的问题,那如何进行分支流程呢?
将PARA_FOLLOW()宏中跟随的0和1替换为分支语句,这样就完成了完整的条件分支。
1 |
|
SELECT_012_PARA()宏可以根据可以参数列表中参数的数量来生成不同的代码,而不关心参数具体的值。
据此,我重新设计了INCFILE()宏:
1 |
INCFILE()可以使用不同的参数数量,来选择是否需要跳过头部数据。
1 | // hello.txt content is 12345678hellohello12345 |
一个宏函数实现了多种功能,看起来有点多态的性质。
至此,一个在嵌入式软件开发过程中一个不起眼的问题就被成功解决了。
宏定义是否可以实现递归?
完成条件分支功能后,我进行了进一步的思考,在宏定义中能否实现递归呢?
请设计一个宏,该宏可以传入任意个整数,返回一个表达式,表示其最大值。
C语言的初学者可能都写过这样的宏:
1 |
|
这只能接受2个参数,如果传入3个参数,就会报错。任意个参数的递归版本也许是这样的:
1 |
实际展开情况呢?
1 | /* |
宏定义看起来无法正常的递归展开。这时候去检索相关的资料发现,C语言的宏定义确实无法直接实现递归。
但是在查询资料的时候,我发现通过一种通过延迟求值的技巧,可以做到有限的递归。
说到延迟求值,那就有必要先回顾一下C语言宏定义的求值规则。
宏定义的求值规则
先展开内层实参,再展开外层宏函数。如果形参前有#、#@或者形参前后有##,则对应实参不展开。
宏函数求值规则特别复杂,简单来说,先计算实参的值,替换到宏函数中,然后重新进行计算。
每当宏展开为非宏的标识符后,将触发重新扫描,从开始处重新计算。当对宏函数A()求值时遇到A,会将其标记为非宏标识符以避免递归,即使从外层重新对其进行扫描也无法对其继续展开。
一个例子进行求值说明:
1 |
|
再看一个许多网友都没有正确理解的例子:
1 |
|
在编写宏函数时需要掌握一个技巧,在涉及到宏嵌套和需要在代码中直接访问的宏函数来说,其参数不能直接含有#、#@、##,需要通过另一个宏函数间接访问。在编写宏函数时注意这一点可以避免很多意料之外的求值顺序问题。
1 |
|
以上这样的宏定义求值都太简单了。真正的大招在后面~
对宏定义递归的探索
在StackOverflow上,有人提出是否可以实现这样的递归宏定义:
1 |
我们可以根据之前学习的规则来尝试展开pr(5):
1 | /* |
为了避免这个问题,大佬们提出了一种延时求值的技巧。
1 |
可以尝试再一次展开pr(5):
1 | /* |
EXPAND()的作用就是进行一次展开。显然上面的递归定义需要多次展开。所以可以将多个EXPAND叠加在一起组合为一个新的宏函数。
1 |
调用EVAL()可以进行多少次求值呢?243次。
这里通过一次DEFER操作实现了对pr的延时求值,使得在外部通过调用EXPAND来在外部展开,避免了无法直接递归的问题。但是很显然,在外部手动调用EXPAND的次数是有限的,不能无限递归。
至此,到这里仅仅完成了递归嵌套的定义,对于递归来说,最重要的是递归中止条件,这才是用宏函数实现递归的一大难点。在进一步探索递归前,我们学习一下大佬们实现的用宏函数完成的循环语句。
使用宏定义实现循环语句
参考内容:Is the C99 preprocessor Turing complete?
有大佬实现了一个循环语句,通过宏定义实现。这里把其中的关键代码整理如下:
1 |
来看看其强大之处:
1 |
|
这个循环的内在逻辑这里就不多解释了,多看看就会了,有很多的技巧。
当我掌握了这里面的一些技巧后,我开始设计一个求多个数最大值的宏函数。这并不容易,需要使用递归实现并完成递归中止条件的处理。
宏函数递归实现求最大值
思路如下:
- 1.采用延时求值表达式完成正常的递归表达;
- 2.完成递归中止条件,通过计算参数个数来判断递归是否结束;
- 3.处理递归内部分支流程,中止条件完成后需要进行分支流程,一种是继续递归,一种是当只有一个参数时直接返回。
首先是递归表达式:
1 |
这里应用了延时求值的技巧。
接下来是递归中止条件,需要判断参数个数是否为1,先实现一个判断宏参数是否存在的宏函数:
1 |
|
CHECK_ARGS()宏函数可以判断宏参数个数,如果有一个或多个参数则返回1,参数列表为空则返回0。
接下来使用CHECK_ARGS()来实现一个判断参数是否只有一个的宏(处理递归结束):
1 |
|
接下来通过CHECK_ARGS1()配合CAT()宏,拼接出MAX_1和MAX_0两个分支:
1 |
至此MAX_NUM()就能够完成主要的功能,由于延迟求值的原因,需要在外层再套一个EVAL()。每多一个参数就多一层递归,也就多一次延迟求值。前面写的EVAL()可以处理243次延迟求值。
1 |
|
可以看出,递归展开后呈二次增长。无论生成的表达式多复杂,结果都是正确的。受限于CHECK_ARGS(),这只能处理不超过63个数的最大值计算。
这里还存在一点点瑕疵,前面是先递归再分支判断,所以传入一个或零个参数时的结果不正确。可以先直接调用MAX_INDIRECT()做一次分支判断,当只有一个参数时就直接返回。
1 |
|
数值系统
MAX()真正意义上来说没有得到一个数列的最大值,原因是C语言的宏定义没有数值计算系统。所以这里返回了一个表示最大值表达式。
进一步思考一下,宏定义能不能实现数值计算?
1 |
这并不是我想要的,这仅仅返回了一个表达式。我需要的是能够直接返回计算结果。
1 | /* |
注意前文的两个宏函数INC()、DEC(),它们能够实现一个数的加1或减1:
1 | /* |
那么ADD(a, b)函数可以表达为需要调用a次的INC(b)。或者说a+b=(a-1)+(b+1),ADD(a, b)=ADD(DEC(a), INC(b))。这看起来又是一种递归的形式了,当a等于0时结束递归。这是一种尾递归的形式,可以使用表达为循环。来看看C语言的形式:
1 | int add(int a, int b){ |
这样的形式就很容易使用宏定义表达:
1 |
|
思考一下,能不能实现一个数值版本的MAX()宏函数?它能真正计算出数列的最大值。
1 | /* |
这似乎是一个新的挑战,需要完成宏定义中没有的数值比较功能。后面有时间再研究。😅
总结
仅用#define功能就完成了条件语句、循环、递归的实现。这是不是意味着C语言的宏是图灵完备的呢?
我查阅了很多网友的讨论,大部分人的看法是有限个数的宏标识符无法完成尽可能大的递归深度。通过C语言宏实现的counter机制要求每个值都需要一个宏标识符,这就限制了宏的复杂性。因为C语言的规范中限定了单次宏处理标识符的最大数量。这和其它图灵机的无限内存是有本质区别的。
此外,前面所实现的循环和递归都是有限深度的。无法实现无限循环和无穷递归就不具备完整的图灵完备性。但它确实完成了迭代、条件等类似的结构,这是不是意味着它是存在有限的图灵完备性。
本文仅讨论C语言宏的一些内在性质,并不推荐在实际的开发中使用如此复杂的代码。
参考
1.theory - 什么是图灵完备?
2.C/C++ 宏的图灵完备性
3.Is the C99 preprocessor Turing complete?
4.Dave Prosser’s C Preprocessing Algorithm
5.Best abuse of the C preprocessor