前言

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

对于规则 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 即空参数

宏的运行

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

  1. 预处理器会对代码进行多遍扫描,展开所有遇到的宏,直到触发以下条件

    1. 到达展开次数上限
    2. 遇到自参照宏,即宏曾经展开过的模样
  2. 宏函数展开前先对所有参数进行一次预扫描并展开,除非遇到以下条件
    1. 用于拼接的参数不展开 (##)
    2. 用于宏字面量的参数不展开 (#)
  3. 在宏函数展开后,替换后的文本会进行后扫描 (二次扫描),对遇到的宏继续进行展开
  4. 预扫描和后扫描都遵循条件 a,会进行多遍扫描
  5. 每次扫描前后,都会进行宏的语法检查

下面分别对每条规则进行说明。

自参照宏

写个简单的宏代码测试下:

#define X0 X1
#define X1 X2
#define X2 X3
#define X3 X0 X0 // -> X0
X1 // -> X1
X2 // -> X2
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 自参照,停止替换

将最后一行宏定义改为:

#define X3 X1

输出变为:

X1
X1
X2
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 脚本批量制造测试代码:

for((i=0;i<10000;++i)); do echo "#define X$i X$((i+1))"; done | pbcopy

mac 上的 pbcopy 负责将代码复制到剪贴板,也可以直接重定向到文件再复制出来。在 Compiler Explorer 中运行:

居然没问题,看起来 10000 并不是上限。切换编译器为 x86-64 clang (trunk) 也正常,直到 x64 msvc v19.latest 时失败:

<source>(10002): fatal error C1009: compiler limit: macros nested too deeply

经过多次探测,msvc 最终的最大宏定义是 X255,也就是说扫描次数最大为 256。

不过 Compiler Explorer 上提供的 msvc 最新只到 VS2017,不过用实体机上的 VS2019 测试,结果也是一样的,看起来 msvc 对宏编程支持一般,怪不道 BOT Man 说 BOOST_PP 里有很多对 msvc 的兼容和 work around,汗~

延迟拼接

规则 b 使用 BOT Man 的例子就不错:

#define FOO(SYMBOL) foo_ ## SYMBOL
#define LITERAL(SYMBOL) #SYMBOL
#define BAR() bar FOO(bar) // -> foo_bar
FOO(BAR()) // -> foo_BAR()
LITERAL(BAR()) // -> "BAR()"

如果参数是用于拼接或取字面量的,预扫描将不会对它进行展开。

注意,这里预处理器对宏参数的预扫描,也遵循规则 a,即在没有 ## 和 # 干扰时,它会一直展开直到 1) 达到最大展开次数 2)遇到自参照宏 时结束,并不是字面意思只扫描一次,这条就是规则 d。

如果希望预处理器忽略 ## & # 操作展开所有传入的宏函数参数,则需借助延迟拼接技术,这个技术听起来很高大上,其实原理很简单,直白说就是不直接实现宏函数而是调用另一个宏函数实现之:

#define FOO(SYMBOL) FOO_IMPL(foo_, SYMBOL)
#define FOO_IMPL(A, B) A##B FOO(bar) // -> foo_bar
FOO(BAR()) // -> foo_bar

一般命名此类宏函数的惯例是: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 前缀,可以这样做:

#define TO_UNICODE(x) L##x
#define PRODUCT_NAME "Chrome"
// #define PRODUCT_NAME_W TO_UNICODE("Chrome")
#define PRODUCT_NAME_W TO_UNICODE(PRODUCT_NAME)
std::wstring product_name = PRODUCT_NAME_W; // -> LPRODUCT_NAME

使用 TO_UNICODE 宏为字符常量添加 L 前缀时,如果作用于常量字符串,是正常的;如果作用于经过宏定义的字符串 (PRODUCT_NAME),基于规则 b.i 就会出现非预期结果 (LPRODUCT_NAME),为了解决 L 拼接时宏不展开的问题,就需要借助延迟拼接技术:

#define TO_UNICODE_IMPL(y) L##y
#define TO_UNICODE(x) TO_UNICODE_IMPL(x)
#define PRODUCT_NAME "Chrome"
#define PRODUCT_NAME_W TO_UNICODE(PRODUCT_NAME)
std::wstring product_name = PRODUCT_NAME_W; // -> L"Chrome"

关于这个例子,具体可参考附录 7。

惰性求值

惰性求值与后处理相关,技术不难理解,难的是场景不好说明,先来看一个宏语法错误:

#define PP_COMMA() ,
#define PP_EMPTY() #define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B)
#define PP_CONCAT_IMPL(A, B) A##B PP_CONCAT(x PP_COMMA() y)
PP_CONCAT(x, PP_COMMA())

预处理器会报下面的错误:

<source>:25:25: error: macro 'PP_CONCAT' requires 2 arguments, but only 1 given
25 | PP_CONCAT(x PP_COMMA() y)
| ^
<source>:4:9: note: macro 'PP_CONCAT' defined here
4 | #define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B)
| ^~~~~~~~~
<source>:26:24: error: macro 'PP_CONCAT_IMPL' passed 3 arguments, but takes just 2
26 | PP_CONCAT(x, PP_COMMA())
| ^
<source>:5:9: note: macro 'PP_CONCAT_IMPL' defined here
5 | #define PP_CONCAT_IMPL(A, B) A##B
| ^~~~~~~~~~~~~~

第一个表达式出错,是错在预扫描前进行的语法检查,此时 PP_COMMA() 还未替换为 ',' 整个是一个参数:x PP_COMMA() yPP_CONCAT 要求 2 个参数而只提供了 1 个;

第二个表达式出错,是错在预扫描后进行的语法检查,此时 PP_COMMA() 替换为了 ',' 整个是三个参数:xPP_CONCAT 要求 2 个参数而提供了 3 个。

也就是说,宏的语法检查是时刻进行的,在每次替换前后都会进行,这就是规则 e。有了这个基础,再看下面这个例子,它更贴近真实场景:

#define PP_COMMA() ,
#define PP_EMPTY() #define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B)
#define PP_CONCAT_IMPL(A, B) A##B #define PP_BOOL(N) PP_CONCAT(PP_BOOL_, N)
#define PP_BOOL_0 0
#define PP_BOOL_1 1
#define PP_BOOL_2 1 #define PP_IF(PRED, THEN, ELSE) PP_CONCAT(PP_IF_, PP_BOOL(PRED))(THEN, ELSE)
#define PP_IF_1(THEN, ELSE) THEN
#define PP_IF_0(THEN, ELSE) ELSE #define PP_COMMA_IF(N) PP_IF(N, PP_COMMA(), PP_EMPTY())
#define log(format, n, ...) printf(format PP_COMMA_IF(n) __VA_ARGS__) log("%d%f", 2, 1, .2);
log("hello", 0);

重点是 log 宏函数的实现,委托给了 printf,在格式 format 与参数 __VA_ARGS__ 之间,要不要加个逗号做分隔,完全看用户传递的参数个数,大于 0 则需要,否则不需要,不然后期会出现编译期语法错误。

这里为了简化例子,参数个数是用户手动传递的,参考上面的两个 case,分别有 2 个参数和 0 个参数。

现在焦点就集中在 PP_COMMA_IF 宏的实现上了,它根据 n 的值,决定输出 PP_COMMA(),还是 PP_EMPTY(),这之前那一堆宏都是为了实现 PP_IF,看不懂也没关系。

到这里似乎没有什么问题,然而编译却报错:

<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
| ^~~~~~~

错误有点不知所云,列个表推理下宏展开过程,先看第一个 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 看得更明白:

clang++: warning: argument unused during compilation: '-S' [-Wunused-command-line-argument]
<source>:21:1: error: too many arguments provided to function-like macro invocation
21 | log("%d%f", 2, 1, .2);
| ^
<source>:20:43: note: expanded from macro 'log'
20 | #define log(format, n, ...) printf(format PP_COMMA_IF(n) __VA_ARGS__)
| ^
<source>:17:24: note: expanded from macro 'PP_COMMA_IF'
17 | #define PP_COMMA_IF(N) PP_IF(N, PP_COMMA(), PP_EMPTY())
| ^
<source>:12:70: note: expanded from macro 'PP_IF'
12 | #define PP_IF(PRED, THEN, ELSE) PP_CONCAT(PP_IF_, PP_BOOL(PRED))(THEN, ELSE)
| ^
<source>:14:9: note: macro 'PP_IF_1' defined here
14 | #define PP_IF_1(THEN, ELSE) THEN
| ^
1 error generated.

PP_COMMA() 似乎是延迟到 PP_IF_1 中后才展开,这个和我的理解有 gap,有了解的大神还望不吝指点。

不管怎么说,预扫描展开宏函数参数导致参数个数不匹配,导致了这个问题,而我们又不打算拼接参数或取字面量,所以无法通过规则 b 解决问题。

好在传递给 PP_IF 的这两个参数,都是宏函数,这种场景下可以只传递宏函数名,括号放在 PP_IF 后,让宏函数在后扫描中再生效:

#define PP_COMMA_IF(N) PP_IF(N, PP_COMMA, PP_EMPTY)()

这就是规则 c,现在能得到正确的结果了:

printf("%d%f" , 1, .2);
printf("hello" );

下面仍为第一个 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_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)() ); #define PP_COMMA_IF(N) PP_IF(N, PP_COMMA, PP_EMPTY)()
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_1PP_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 库的精减版,后者有更为强大的代码生成能力,感兴趣的读者可以进一步探索,这里只举几个例子:

// case 1
#include <boost/preprocessor/repetition/repeat.hpp>
#define DECL(z, n, text) text ## n = n;
BOOST_PP_REPEAT(5, DECL, int x)
---
int x0 = 0; int x1 = 1; int x2 = 2; int x3 = 3; int x4 = 4; // case 2
#include <boost/preprocessor/repetition/enum_params.hpp>
template <BOOST_PP_ENUM_PARAMS(3, class T)>
struct a{};
---
template < class T0 , class T1 , class T2>
struct a{}; // case 3
#include <boost/preprocessor/arithmetic/inc.hpp>
#include <boost/preprocessor/repetition/enum_params.hpp>
#include <boost/preprocessor/repetition/repeat.hpp> #define MACRO(z, n, _) \
return_type constructor(\
BOOST_PP_ENUM_PARAMS_Z(z, BOOST_PP_INC(n),type param))\
{;}; BOOST_PP_REPEAT(2, MACRO, nil)
---
return_type constructor(type param0) { ; };
return_type constructor(type param0, type param1) { ; }; // case 4
#include <boost/preprocessor/arithmetic/inc.hpp>
#include <boost/preprocessor/repetition/enum.hpp> #define TEXT(z, n, text) text
#define TTP(z, n, _) \
template< BOOST_PP_ENUM_ ## z(BOOST_PP_INC(n), TEXT, class) > \
class T ## n
BOOST_PP_ENUM(3, TTP, nil)
---
template <class> class T0 ,
template <class, class> class T1 ,
template <class, class, class> class T2 // case 5
#define n BOOST_PP_ITERATION()
#define TINY_print(z, n, data) data template<BOOST_PP_ENUM_PARAMS(n, class T)>
struct tiny_size< BOOST_PP_ENUM_PARAMS(n, T) BOOST_PP_COMMA_IF(n) BOOST_PP_ENUM(BOOST_PP_SUB(M, n), TINY_print, none)>
: mpl::int_ <n>
{ }; #undef n // case 5.1
#include <boost/preprocessor/repetition.hpp>
#include <boost/preprocessor/arithmetic/sub.hpp>
#include <boost/preprocessor/punctuation/comma_if.hpp>
#include <boost/preprocessor/iteration/iterate.hpp> #define M 3 #define BOOST_PP_ITERATION_LIMITS (0,M-1)
#define BOOST_PP_FILENAME_1 "pattern.h"
#include BOOST_PP_ITERATE()
---
template<>
struct tiny_size< none , none , none>
: mpl::int_ <0>
{
};
template< class T0>
struct tiny_size< T0 , none , none>
: mpl::int_ <1>
{
};
template< class T0 , class T1>
struct tiny_size< T0 , T1 , none>
: mpl::int_ <2>
{
};

看看有没有满足你需求的 (我也没看懂原理,库嘛,拿来用就好了)。

参考

[1]. 360 安全规则集合

[2]. C++ 下 typeof 的实现

[3]. Replacing text macros

[4]. C/C++ 宏编程的艺术

[5]. 《产生式元编程》第一章 宏编程计数引原理

[6]. Self-Referential Macros

[7]. 使用C++宏嵌套实现窄字符转换为宽字符

[8]. Boost Preprocessor (PP库) 中的奇技淫巧

你所不知道的 C/C++ 宏知识——基于《C/C++ 宏编程的艺术》的更多相关文章

  1. SIM卡是什么意思?你所不知道的SIM卡知识扫盲(详解)【转】

    原文链接:http://www.jb51.net/shouji/359262.html 日常我们使用手机,SIM卡是手机的必须,没有了它就不能接入网络运营商进行通信服务.SIM卡作为网络运营商对于我们 ...

  2. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

  3. 你所不知道的setTimeout

    JavaScript提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()和setInterval()这两个函数来完成.它们向任务队列添加定时任务.初始接触它的人都觉得好简单 ...

  4. 你所不知道的SQL Server数据库启动过程(用户数据库加载过程的疑难杂症)

    前言 本篇主要是上一篇文章的补充篇,上一篇我们介绍了SQL Server服务启动过程所遇到的一些问题和解决方法,可点击查看,我们此篇主要介绍的是SQL Server启动过程中关于用户数据库加载的流程, ...

  5. 你所不知道的 CSS 滤镜技巧与细节

    承接上一篇你所不知道的 CSS 动画技巧与细节,本文主要介绍 CSS 滤镜的不常用用法,希望能给读者带来一些干货! OK,下面直接进入正文.本文所描述的滤镜,指的是 CSS3 出来后的滤镜,不是 IE ...

  6. js类型----你所不知道的JavaScript系列(5)

    ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型.也有其他的叫法,比如原始类型和对象类型等. 1.内置类型 JavaScript 有七种内置类型: • 空值(null) • 未定义( ...

  7. 关于setTimeout()你所不知道的地方,详解setTimeout()

    关于setTimeout()你所不知道的地方,详解setTimeout() 前言:看了这篇文章,1.注意setTimeout引用的是全部变量还是局部变量了,当直接调用外部函数方法时,实际上函数内部的变 ...

  8. [转帖]QC 和 PD:关于你所不知道的快充

    QC 和 PD:关于你所不知道的快充 http://www.sohu.com/a/276214250_465976 2018-11-18 06:02 当我们使用支持 PD 或者 QC 快充协议的电源适 ...

  9. 你所不知道的setInterval

    在你所不知道的setTimeout记载了下setTimeout相关,此篇则整理了下setInterval:作为拥有广泛应用场景(定时器,轮播图,动画效果,自动滚动等等),而又充满各种不确定性的这set ...

  10. 你真的会玩SQL吗?你所不知道的 数据聚合

    你真的会玩SQL吗?系列目录 你真的会玩SQL吗?之逻辑查询处理阶段 你真的会玩SQL吗?和平大使 内连接.外连接 你真的会玩SQL吗?三范式.数据完整性 你真的会玩SQL吗?查询指定节点及其所有父节 ...

随机推荐

  1. 推荐一个跨平台支持Word, Excel, CSV, Email等30多种格式的操作库

    更多开源项目请查看:一个专注推荐优秀.Net开源项目的榜单 在我们日常项目开发中,经常需要解析操作文档,比如Office文档.Email文件.PDF.Xml.图片.Mp3等音频文件,操作Office. ...

  2. 关于Air780E:与服务器的加密通信操作方法

    ​ 今天我们来学习合宙低功耗4G模组Air780E快速入门之跟服务器之间的加密通信,伙伴们,一起学起来! 一.编写脚本 1.1 准备资料 Air780E开发板购买 Air780E开发板设计资料 Lua ...

  3. MoD:轻量化、高效、强大的新型卷积结构 | ACCV'24

    来源:晓飞的算法工程笔记 公众号,转载请注明出处 论文: CNN Mixture-of-Depths 论文地址:https://arxiv.org/abs/2409.17016 创新点 提出新的卷积轻 ...

  4. 3张大图剖析HttpClient和IHttpClientFactory在解决DNS解析问题上的殊途同归

    在开发者便利度角度,我们很轻松地使用HttpClient对象发出HTTP请求,只需要关注应用层协议的BaseAddr.Url.ReqHeader.timeout. 实际在HttpClient在源码级别 ...

  5. 从2s优化到0.1s

    前言 分类树查询功能,在各个业务系统中可以说随处可见,特别是在电商系统中. 但就是这样一个简单的分类树查询功能,我们却优化了5次. 到底是怎么回事呢? 背景 我们的网站使用了SpringBoot推荐的 ...

  6. (Python基础教程之十六)Python multidict示例–将单个键映射到字典中的多个值

    1.什么是multidict词典> 在python中," multidict "一词用于指代字典,在字典中可以将单个键映射到多个值.例如 多重结构 multidictWith ...

  7. Java并发基础构建模块简介

    在实际并发编程中,可以利用synchronized来同步线程对于共享对象的访问,用户需要显示的定义synchronized代码块或者方法.为了加快开发,可以使用Java平台一些并发基础模块来开发. 注 ...

  8. MongoDB学习笔记之 第1章 MongoDB的安装

    MongoDB学习笔记之 第1章 MongoDB的安装 MongoDB学习笔记之 第2章 MongoDB的增删改查 MongoDB学习笔记之 第3章 MongoDB的Java驱动 MongoDB学习笔 ...

  9. \r,\n,\r\n的前世今生

    前情 最近在逛论坛的时候遇到有人在提问题,为什么\n在苹果手机上不换行,我以前有网上看到过文章,是因为各系统的解析不同,需要使用\r\n来做兼容,自己虽然知道怎么解决,但是不知具体原因,今特来详细了解 ...

  10. 【双堆懒删除】codeforces 1294 D. MEX maximizing

    前言 双堆懒删除 当需要维护若干元素中的最大值(或最小值)时,可以用一个堆维护,但是堆只擅长处理堆顶元素,对堆中任意元素的处理就束手无策了.此时,可以引入另外一个堆,我们定义原来的堆为保存堆 \(ex ...