CC++宏编程中,有哪些未知的技巧或特性,你却未曾了解过?
摘要:CC++ 宏缺陷这么多,它过时了吗?预处理器如何替换宏,有次数限制吗?何时终止?何为预扫描、后扫描?如何利用它们来实现延迟拼接、惰性求值,这些技术又有什么用处,宏与 C+&#x
前言
刚学 C++ 的时候,就知道它糅合了四种编程模式:基于预处理器的宏、基于 C 语言的面向过程、基于类的面向对象、以及基于模板的泛型编程。其中,宏和模板元编程因为是在编译期出结果,能有效提升程序运行期性能,有着独特的价值。
宏的缺陷
之前了解的宏编程,大多数在数说它的缺陷,以及如何避免,以下面的宏为例
#define max(a,b) a>b?a:b
就中了不少的招:
参数要用括号包围,例如 max(2+1,2)
宏表达式要用括号包围,例如 max(1,2)*3
多次求值,例如 max(n++,2)
多个语句的宏应该用 do{}while(0) 包含,例如 if (x>y) SWAP(x,y),其中 SWAP 宏的实现至少需要三条语句
不受命名空间限制,命名易冲突,例如参数名也使用 max 时会被展开
...
总而言之就是少用宏、不用宏,为此想出了各种方式来替代宏:
typedef 与 using 定义类型别名
inline 函数内联短小函数提升执行效率
const 定义常量
...
即使是使用宏,也加入很多改进,例如 GNU C 引入了 typeof 关键字,来解决参数多次求值、未被括号包围等问题
#define max(x, y) ({ \
typeof(x) _max1 = (x); \
typeof(y) _max2 = (y); \
(void) (&_max1 == &_max2); \
_max1 > _max2 ? _max1 : _max2; })
宏很像结构化编程中的 goto 语句,不能说过街老鼠,也是日暮西山了。
宏的能力
直到我看了一篇文章:《C/C++ 宏编程的艺术》,才发现宏原来还可以这么玩。
作者 BOT Man 主要是谈 GMOCK_PP 库中各个宏的依赖关系,我整理如下:
复杂的如 PP_WHILE->PP_ADD/PP_SUB->PP_MUL->PP_EQUAL/PP_CMP->PP_LESS->PP_DIV/PP_MOD,就没画了,主要是没看懂~
另外,使用宏实现 256 以内的算术运算,有什么实际意义吗?我是持保留态度的。
本文不会鹦鹉学舌再重复一遍 BOT Man 论证过的逻辑,而是梳理下预处理器的工作原理与宏编程遵循的规则,有一些是之前没注意到的,总结出来自己都感觉新鲜,呵呵。
宏的语法
宏虽然只进行文本替换,没有类型的概念,但也有以下基本的语法规则
宏参数使用逗号分隔,因此参数不能再包含逗号,除非使用元组
宏参数不能包含不匹配的括号
非可变参数的宏函数,参数个数必需严格匹配声明
对于规则 I,有些读者可能觉得没必要,毕竟参数名中也不可能有逗号,但是别忘了参数也可能是模板的实例,像下面这样:
#define FOO(return,param) return foo(param);
FOO(bool, std::pair<int, int>)
第二个模板参数中的逗号会使 FOO 的参数个数变为 3,从而导致预处理器报错:
<source>:2:30: error: macro 'FOO' passed 3 arguments, but takes just 2
2 | FOO(bool, std::pair<int, int>)
| ^
<source>:1:9: note: macro 'FOO' defined here
1 | #define FOO(return,param) return foo(param);
|
这里使用了 BOT Man 也推荐的 Compile Explorer 在线编译环境,编译器选择的是 x86-64 gcc trunk、编译参数是 -E -P -std=c++20,后面统一使用这个设置进行测试。
上例中如果使用元组,就不会报错了:
FOO(bool, (std::pair<int, int>))
// => bool foo((std::pair<int, int>));
结果多了一对儿括号,追求完美的人,可以使用 PP_REMOVE_PARENS 去除,这个宏的实现,后面还会涉及,现在先不展开。
