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 去除,这个宏的实现,后面还会涉及,现在先不展开。
阅读全文