你所不知道的 C/C++ 宏知识——基于《C/C++ 宏编程的艺术》 ...

打印 上一主题 下一主题

主题 894|帖子 894|积分 2682

前言

刚学 C++ 的时间,就知道它糅合了四种编程模式:基于预处理器的宏、基于 C 语言的面向过程、基于类的面向对象、以及基于模板的泛型编程。其中,宏和模板元编程由于是在编译期出结果,能有用提升程序运行期性能,有着独特的价值。
宏的缺陷

之前了解的宏编程,大多数在数说它的缺陷,以及怎样避免,以下面的宏为例
  1. #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 关键字,来解决参数多次求值、未被括号包围等问题
  1. #define max(x, y) ({            \
  2.     typeof(x) _max1 = (x);      \
  3.     typeof(y) _max2 = (y);      \
  4.     (void) (&_max1 == &_max2);  \
  5.     _max1 > _max2 ? _max1 : _max2; })
复制代码
宏很像布局化编程中的 goto 语句,不能说过街老鼠,也是日暮西山了。
宏的能力

直到我看了一篇文章:《C/C++ 宏编程的艺术》,才发现宏原来还可以这么玩。

作者 BOT Man 重要是谈 GMOCK_PP 库中各个宏的依赖关系,我整理如下:

复杂的如 PP_WHILE->P_ADD/PP_SUB->P_MUL->P_EQUAL/PP_CMP->P_LESS->P_DIV/PP_MOD,就没画了,重要是没看懂~
另外,使用宏实现 256 以内的算术运算,有什么实际意义吗?我是持保留态度的。
本文不会鹦鹉学舌再重复一遍 BOT Man 论证过的逻辑,而是梳理下预处理器的工作原理与宏编程遵循的规则,有一些是之前没注意到的,总结出来自己都感觉新鲜,呵呵。
宏的语法

宏虽然只举行文本替换,没有类型的概念,但也有以下基本的语法规则

  • 宏参数使用逗号分隔,因此参数不能再包含逗号,除非使用元组
  • 宏参数不能包含不匹配的括号
  • 非可变参数的宏函数,参数个数必须严格匹配声明
对于规则 I,有些读者可能以为没须要,究竟参数名中也不可能有逗号,但是别忘了参数也可能是模板的实例,像下面这样:
  1. #define FOO(return,param) return foo(param);
  2. FOO(bool, std::pair<int, int>)
复制代码
第二个模板参数中的逗号会使 FOO 的参数个数变为 3,从而导致预处理器报错:
  1. <source>:2:30: error: macro 'FOO' passed 3 arguments, but takes just 2
  2.     2 | FOO(bool, std::pair<int, int>)
  3.       |                              ^
  4. <source>:1:9: note: macro 'FOO' defined here
  5.     1 | #define FOO(return,param) return foo(param);
  6.       |   
复制代码
这里使用了 BOT Man 也推荐的 Compile Explorer 在线编译环境,编译器选择的是 x86-64 gcc trunk、编译参数是  -E -P -std=c++20,后面统一使用这个设置举行测试。

上例中如果使用元组,就不会报错了:
  1. FOO(bool, (std::pair<int, int>))
  2. // => bool foo((std::pair<int, int>));
复制代码
结果多了一对儿括号,追求完美的人,可以使用 PP_REMOVE_PARENS 去除,这个宏的实现,后面还会涉及,现在先不展开。
对于规则 III,简单增补下宏的 4 种形态、以及规则的应用环境:
形态说明规则 III
#define identifier replacement-list (optional)仅文本替换--
#define identifier (parameters ) replacement-list (optional)固定参数个数的宏函数参数个数严格匹配
#define identifier (parameters , ...) replacement-list (optional) 部门可变参数个数的宏函数参数个数不能少于已出现的固定参数个数
#define identifier (...) replacement-list (optional)全可变参数个数的宏函数参数个数不做要求,可为 0 即空参数
宏的运行

宏运行时遵循的规则先都列出来:

  • 预处理器会对代码举行多遍扫描,展开全部遇到的宏,直到触发以下条件

    • 到达展开次数上限
    • 遇到自参照宏,即宏曾经展开过的容貌

  • 宏函数展开前先对全部参数举行一次预扫描并展开,除非遇到以下条件

    • 用于拼接的参数不展开 (##)
    • 用于宏字面量的参数不展开 (#)

  • 在宏函数展开后,替换后的文本会举行后扫描 (二次扫描),对遇到的宏继续举行展开
  • 预扫描和后扫描都遵循条件 a,会举行多遍扫描
  • 每次扫描前后,都会举行宏的语法查抄
下面分别对每条规则举行说明。
自参照宏

写个简单的宏代码测试下:
  1. #define X0 X1
  2. #define X1 X2
  3. #define X2 X3
  4. #define X3 X0
  5. X0    // -> X0
  6. X1    // -> X1
  7. X2    // -> X2
  8. X3    // -> X3
复制代码
这是一个循环界说,X0->X1->X2->X3->X0,输出已列在代码解释,貌似什么也没发生,以 X0 为准,看下整个替换过程:
状态应用宏
X0初始
X1#define X0 X1
X2#define X1 X2
X3#define X2 X3
X0#define X3 X0
X0自参照,停止替换
将末了一行宏界说改为:
  1. #define X3 X1
复制代码
输出变为:
  1. X1
  2. X1
  3. X2
  4. X3
复制代码
这时 X0 的界说变为为 X1,整个过程列表如下:
状态应用宏
X0初始
X1#define X0 X1
X2#define X1 X2
X3#define X2 X3
X1#define X3 X1
X1自参照,停止替换
换句话说,预处理器会记载每个宏每次展开的历史值,避免与之重复,从而产生无限循环。
这个特性,导致宏无法举行任何递归或重入,要举行任何推导,必须辛辛苦苦写 MACRO_1 / MACRO_2 ... MACRO_N 的代码,且 N 一般有上限。这是和模板元编程区别最大的地方,后者可以重载,举行模板偏特化,从而直接指定推导的结束条件。
展开次数上限

上面的例子中预处理器扫描了 4~5 次,为了观察它的扫描次数上限,使用下面的 shell 脚本批量制造测试代码:
  1. for((i=0;i<10000;++i)); do echo "#define X$i X$((i+1))"; done | pbcopy
复制代码
如果参数是用于拼接或取字面量的,预扫描将不会对它举行展开。
注意,这里预处理器对宏参数的预扫描,也遵循规则 a,即在没有 ## 和 # 干扰时,它会不停展开直到 1) 到达最大展开次数 2)遇到自参照宏 时结束,并不是字面意思只扫描一次,这条就是规则 d。
如果希望预处理器忽略 ## & # 操作展开全部传入的宏函数参数,则需借助耽误拼接技术,这个技术听起来很高大上,其实原理很简单,直白说就是不直接实现宏函数而是调用另一个宏函数实现之:
  1. <source>(10002): fatal error C1009: compiler limit: macros nested too deeply
复制代码
一般命名此类宏函数的惯例是:MACRO & MACRO_IMPL,后者就是真正干活的宏了。下面列表推理下展开过程:
状态应用宏
FOO(BAR())初始
FOO(bar)#define BAR bar 且 FOO 的实现没有对参数拼接或取字面量的操作
FOO_IMPL(foo_, bar)#define FOO(SYMBOL)  FOO_IMPL(foo_, SYMBOL)
foo_bar#define FOO_IMPL(A, B) A##B
另外一个有实战意义的例子是在 Windows 上常用的宽字符前缀 L,如果界说一个 TO_UNICODE 的宏为恣意窄字符串增加 L 前缀,可以这样做:
  1. #define FOO(SYMBOL) foo_ ## SYMBOL
  2. #define LITERAL(SYMBOL) #SYMBOL
  3. #define BAR() bar
  4. FOO(bar)    // -> foo_bar
  5. FOO(BAR())  // -> foo_BAR()
  6. LITERAL(BAR())  // -> "BAR()"
复制代码
使用 TO_UNICODE 宏为字符常量添加 L 前缀时,如果作用于常量字符串,是正常的;如果作用于颠末宏界说的字符串 (PRODUCT_NAME),基于规则 b.i 就会出现非预期结果 (LPRODUCT_NAME),为了解决 L 拼接时宏不展开的问题,就需要借助耽误拼接技术:
  1. #define FOO(SYMBOL) FOO_IMPL(foo_, SYMBOL)
  2. #define FOO_IMPL(A, B) A##B
  3. FOO(bar)    // -> foo_bar
  4. FOO(BAR())  // -> foo_bar
复制代码
关于这个例子,具体可参考附录 7。
惰性求值

惰性求值与后处理相关,技术不难明白,难的是场景不好说明,先来看一个宏语法错误:
  1. #define TO_UNICODE(x) L##x
  2. #define PRODUCT_NAME "Chrome"
  3. // #define PRODUCT_NAME_W TO_UNICODE("Chrome")
  4. #define PRODUCT_NAME_W TO_UNICODE(PRODUCT_NAME)
  5. std::wstring product_name = PRODUCT_NAME_W;     // -> LPRODUCT_NAME
复制代码
预处理器会报下面的错误:
  1. #define TO_UNICODE_IMPL(y) L##y
  2. #define TO_UNICODE(x) TO_UNICODE_IMPL(x)
  3. #define PRODUCT_NAME "Chrome"
  4. #define PRODUCT_NAME_W TO_UNICODE(PRODUCT_NAME)
  5. std::wstring product_name = PRODUCT_NAME_W;     // -> L"Chrome"
复制代码
第一个表达式出错,是错在预扫描进步行的语法查抄,此时 PP_COMMA() 还未替换为 ',' 整个是一个参数:x PP_COMMA() y,PP_CONCAT 要求 2 个参数而只提供了 1 个;
第二个表达式出错,是错在预扫描后举行的语法查抄,此时 PP_COMMA() 替换为了 ',' 整个是三个参数:x、空、空,PP_CONCAT 要求 2 个参数而提供了 3 个。
也就是说,宏的语法查抄是时候举行的,在每次替换前后都会举行,这就是规则 e。有了这个底子,再看下面这个例子,它更贴近真实场景:
  1. #define PP_COMMA() ,
  2. #define PP_EMPTY()
  3. #define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B)
  4. #define PP_CONCAT_IMPL(A, B) A##B
  5. PP_CONCAT(x PP_COMMA() y)
  6. PP_CONCAT(x, PP_COMMA())
复制代码
重点是 log 宏函数的实现,委托给了 printf,在格式 format 与参数 __VA_ARGS__ 之间,要不要加个逗号做分隔,完全看用户转达的参数个数,大于 0 则需要,否则不需要,不然后期会出现编译期语法错误。
这里为了简化例子,参数个数是用户手动转达的,参考上面的两个 case,分别有 2 个参数和 0 个参数。
现在焦点就集中在 PP_COMMA_IF 宏的实现上了,它根据 n 的值,决定输出 PP_COMMA(),还是 PP_EMPTY(),这之前那一堆宏都是为了实现 PP_IF,看不懂也没关系。
到这里似乎没有什么问题,然而编译却报错:
  1. <source>:25:25: error: macro 'PP_CONCAT' requires 2 arguments, but only 1 given
  2.    25 | PP_CONCAT(x PP_COMMA() y)
  3.       |                         ^
  4. <source>:4:9: note: macro 'PP_CONCAT' defined here
  5.     4 | #define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B)
  6.       |         ^~~~~~~~~
  7. <source>:26:24: error: macro 'PP_CONCAT_IMPL' passed 3 arguments, but takes just 2
  8.    26 | PP_CONCAT(x, PP_COMMA())
  9.       |                        ^
  10. <source>:5:9: note: macro 'PP_CONCAT_IMPL' defined here
  11.     5 | #define PP_CONCAT_IMPL(A, B) A##B
  12.       |         ^~~~~~~~~~~~~~
复制代码
错误有点不知所云,列个表推理下宏展开过程,先看第一个 case:
状态应用宏
log("%d%f", 2, 1, .2); 初始
printf(“%d%f" PP_COMMA_IF(2) 1, .2);#define log(format, n, ...) printf(format PP_COMMA_IF(n) __VA_ARGS__)
printf(“%d%f" PP_IF(2, PP_COMMA(), PP_EMPTY()) 1, .2);#define PP_COMMA_IF(N) PP_IF(N, PP_COMMA(), PP_EMPTY())
printf(“%d%f" PP_IF(2, , , ) 1, .2);#define PP_COMMA() , 和 #define PP_EMPTY() 且 PP_IF 实现没有对参数 ## & #
报错PP_IF 参数个数不匹配,需要 3 个,实际 4 个
推导显示是在 PP_IF 处报错,实际报错信息显示是 PP_IF_1,预处理器似乎走的更远,切换为 clang 看得更明白:
  1. #define PP_COMMA() ,
  2. #define PP_EMPTY()
  3. #define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B)
  4. #define PP_CONCAT_IMPL(A, B) A##B
  5. #define PP_BOOL(N) PP_CONCAT(PP_BOOL_, N)
  6. #define PP_BOOL_0 0
  7. #define PP_BOOL_1 1
  8. #define PP_BOOL_2 1
  9. #define PP_IF(PRED, THEN, ELSE) PP_CONCAT(PP_IF_, PP_BOOL(PRED))(THEN, ELSE)
  10. #define PP_IF_1(THEN, ELSE) THEN
  11. #define PP_IF_0(THEN, ELSE) ELSE
  12. #define PP_COMMA_IF(N) PP_IF(N, PP_COMMA(), PP_EMPTY())
  13. #define log(format, n, ...) printf(format PP_COMMA_IF(n) __VA_ARGS__)
  14. log("%d%f", 2, 1, .2);
  15. log("hello", 0);
复制代码
PP_COMMA() 似乎是耽误到 PP_IF_1 中后才展开,这个和我的明白有 gap,有了解的大神还望不吝指点。
不管怎么说,预扫描展开宏函数参数导致参数个数不匹配,导致了这个问题,而我们又不计划拼接参数或取字面量,所以无法通过规则 b 解决问题。
好在转达给 PP_IF 的这两个参数,都是宏函数,这种场景下可以只转达宏函数名,括号放在 PP_IF 后,让宏函数在后扫描中再见效:
  1. <source>:20:1: error: macro 'PP_IF_1' passed 3 arguments, but takes just 2
  2.    20 | log("%d%f", 2, 1, .2);
  3.       | ^~~~~~~
  4. <source>:13:9: note: macro 'PP_IF_1' defined here
  5.    13 | #define PP_IF_1(THEN, ELSE) THEN
  6.       |         ^~~~~~~
  7. <source>:21:1: error: macro 'PP_IF_0' passed 3 arguments, but takes just 2
  8.    21 | log("hello", 0);
  9.       | ^~~~~~~
  10. <source>:14:9: note: macro 'PP_IF_0' defined here
  11.    14 | #define PP_IF_0(THEN, ELSE) ELSE
  12.       |         ^~~~~~~
复制代码
这就是规则 c,现在能得到正确的结果了:
  1. clang++: warning: argument unused during compilation: '-S' [-Wunused-command-line-argument]
  2. <source>:21:1: error: too many arguments provided to function-like macro invocation
  3.    21 | log("%d%f", 2, 1, .2);
  4.       | ^
  5. <source>:20:43: note: expanded from macro 'log'
  6.    20 | #define log(format, n, ...) printf(format PP_COMMA_IF(n) __VA_ARGS__)
  7.       |                                           ^
  8. <source>:17:24: note: expanded from macro 'PP_COMMA_IF'
  9.    17 | #define PP_COMMA_IF(N) PP_IF(N, PP_COMMA(), PP_EMPTY())
  10.       |                        ^
  11. <source>:12:70: note: expanded from macro 'PP_IF'
  12.    12 | #define PP_IF(PRED, THEN, ELSE) PP_CONCAT(PP_IF_, PP_BOOL(PRED))(THEN, ELSE)
  13.       |                                                                      ^
  14. <source>:14:9: note: macro 'PP_IF_1' defined here
  15.    14 | #define PP_IF_1(THEN, ELSE) THEN
  16.       |         ^
  17. 1 error generated.
复制代码
下面仍为第一个 case 为例,推理下整个展开过程:
状态应用宏
log("%d%f", 2, 1, .2); 初始
printf(“%d%f" PP_COMMA_IF(2) 1, .2);#define log(format, n, ...) printf(format PP_COMMA_IF(n) __VA_ARGS__)
printf(“%d%f" PP_IF(2, PP_COMMA, PP_EMPTY)() 1, .2);<source>:20:1: error: macro 'PP_IF_1' passed 3 arguments, but takes just 2
   20 | log("%d%f", 2, 1, .2);
      | ^~~~~~~
<source>:13:9: note: macro 'PP_IF_1' defined here
   13 | #define PP_IF_1(THEN, ELSE) THEN
      |         ^~~~~~~
<source>:21:1: error: macro 'PP_IF_0' passed 3 arguments, but takes just 2
   21 | log("hello", 0);
      | ^~~~~~~
<source>:14:9: note: macro 'PP_IF_0' defined here
   14 | #define PP_IF_0(THEN, ELSE) ELSE
      |         ^~~~~~~
printf(“%d%f" PP_CONCAT(PP_IF_, PP_BOOL(2))(PP_COMMA, PP_EMPTY)() 1, .2);#define PP_IF(PRED, THEN, ELSE) PP_CONCAT(PP_IF_, PP_BOOL(PRED))(THEN, ELSE)
printf(“%d%f" PP_CONCAT(PP_IF_, PP_CONCAT(PP_BOOL_, 2))(PP_COMMA, PP_EMPTY)() 1, .2);#define PP_BOOL(N) PP_CONCAT(PP_BOOL_, N)
printf(“%d%f" PP_CONCAT(PP_IF_, PP_BOOL_2)(PP_COMMA, PP_EMPTY)() 1, .2);#define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B) 和 #define PP_CONCAT_IMPL(A, B) A##B
printf(“%d%f" PP_CONCAT(PP_IF_, 1)(PP_COMMA, PP_EMPTY)() 1, .2);#define PP_BOOL_2 1
printf(“%d%f" PP_IF_1(PP_COMMA, PP_EMPTY)() 1, .2);#define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B) 和 #define PP_CONCAT_IMPL(A, B) A##B
printf(“%d%f" PP_COMMA() 1, .2);#define PP_IF_1(THEN, ELSE) THEN
printf(“%d%f" , 1, .2);#define PP_COMMA() ,
printf(“%d%f" , 1, .2);无可替换符号,结束
出于练习目标,再看下第二个 case:
状态应用宏
log("hello", 0); 初始
printf(“hello" PP_COMMA_IF(0) );#define log(format, n, ...) printf(format PP_COMMA_IF(n) __VA_ARGS__)
printf(“hello" PP_IF(0, PP_COMMA, PP_EMPTY)() );<source>:20:1: error: macro 'PP_IF_1' passed 3 arguments, but takes just 2
   20 | log("%d%f", 2, 1, .2);
      | ^~~~~~~
<source>:13:9: note: macro 'PP_IF_1' defined here
   13 | #define PP_IF_1(THEN, ELSE) THEN
      |         ^~~~~~~
<source>:21:1: error: macro 'PP_IF_0' passed 3 arguments, but takes just 2
   21 | log("hello", 0);
      | ^~~~~~~
<source>:14:9: note: macro 'PP_IF_0' defined here
   14 | #define PP_IF_0(THEN, ELSE) ELSE
      |         ^~~~~~~
printf(“hello" PP_CONCAT(PP_IF_, PP_BOOL(0))(PP_COMMA, PP_EMPTY)() );#define PP_IF(PRED, THEN, ELSE) PP_CONCAT(PP_IF_, PP_BOOL(PRED))(THEN, ELSE)
printf(“hello" PP_CONCAT(PP_IF_, PP_CONCAT(PP_BOOL_, 0))(PP_COMMA, PP_EMPTY)() );#define PP_BOOL(N) PP_CONCAT(PP_BOOL_, N)
printf(“hello" PP_CONCAT(PP_IF_, PP_BOOL_0)(PP_COMMA, PP_EMPTY)() );#define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B) 和 #define PP_CONCAT_IMPL(A, B) A##B
printf(“hello" PP_CONCAT(PP_IF_, 0)(PP_COMMA, PP_EMPTY)() );#define PP_BOOL_0 0
printf(“hello" PP_IF_0(PP_COMMA, PP_EMPTY)() );#define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B) 和 #define PP_CONCAT_IMPL(A, B) A##B
printf(“hello" PP_EMPTY() 1, .2);#define PP_IF_0(THEN, ELSE) ELSE
printf(“hello"   );#define PP_EMPTY()
printf(“hello"  );无可替换符号,结束
其实重要区别就是 PP_IF_1 与 PP_IF_0 选择 PP_COMMA 还是 PP_EMPTY 的问题。将预扫描变为后扫描,是惰性求值的关键。
宏 VS 模板元

与模板元编程相比,由于规则 a.ii 宏无法递归和重入,要想支持多个参数,必须老诚实实先写 N 个 #define,比力笨。反过来的好处是,他不会生成编译实体,对于控制代码体积有资助。

末了借用 BOT Man 的话对两者做个总结:

C++ 模板元编程 (template metaprogramming) 虽然功能强大,但也有 范围性

  • 不能通过 模板展开 生成新的 标识符 (identifier)

    • 例如 生成新的 函数名、类名、名字空间名 等
    • 使用者 只能使用 预先界说的标识符

  • 不能通过 模板参数 获取 符号/标记 (token)字面量 (literal)

    • 例如 在反射中获取 实参参数名的字面量,在断言中获取 表达式的字面量
    • 使用者 只能通过 转达字符串参数 绕开

所以,在需要直接 操作标识符 的环境下,还需要借助 ,举行 预处理阶段的元编程

  • 编译时 (compile-time)模板 展开差别, 在编译前的 预处理 (preprocess) 阶段全部展开 —— 狭义上,编译器 看不到且不处理 宏代码
  • 通过 #define/TOKEN1##TOKEN2/#TOKEN 界说 宏对象 (object-like macro)宏函数 (function-like macro),可以实现 替换文本、拼接标识符、获取字面量 等功能
总结一下就是:各有所长、结合使用
总结

BOT Man 重要先容的是 Mock PP 库,它是 Boost PP 库的精减版,后者有更为强大的代码生成能力,感爱好的读者可以进一步探索,这里只举几个例子:
  1. #define PP_COMMA_IF(N) PP_IF(N, PP_COMMA, PP_EMPTY)()
复制代码
看看有没有满意你需求的 (我也没看懂原理,库嘛,拿来用就好了)。
参考

[1]. 360 安全规则集合
[2]. C++ 下 typeof 的实现
[3]. Replacing text macros
[4]. C/C++ 宏编程的艺术
[5]. 《产生式元编程》第一章 宏编程计数引原理
[6]. Self-Referential Macros
[7]. 使用C++宏嵌套实现窄字符转换为宽字符
[8]. Boost Preprocessor (PP库) 中的奇技淫巧
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

三尺非寒

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表