木木!
2018-08-19 18:19:37
提到C++宏,大多数人想到的就是宏函数和宏常量,如 #define MAXN 500
和 #define max(a,b) ((a)>(b)?(a):(b))
这种 会出锅的 宏应用。
其实宏还有很多很多更能发挥它威力的应用。宏的用途可不仅限于 constexpr
。几乎任何有重复代码的地方都能用宏大幅度简化,从而节省代码量、提高可复用性。
在继续学习之前,先了解一下 C++ 的宏机制。
(图片来源于网络,侵权删。)
C++的编译流程主要分为预处理、编译、汇编、链接等步骤,其中宏展开在预处理步骤中进行。预处理步骤主要处理预处理指令,就是最前面有引号的指令,包括宏所用的 #define
以及头文件用的 #include
等。
因此,宏在预处理过程中就全部展开。这时候编译器只进行了词法分析(将输入源程序中的数字标识符等识别出来),没有进行语法分析等,因此预处理器执行的只是简单的字符串替换。
具体到每个宏,预处理器若识别出一个符号为宏名,就执行宏展开。对于每一个宏名后面的括号里的内容,预处理器根据且只根据逗号分割参数。如果有括号,括号内的逗号不会被当作分割符。也就是说这样的宏调用是合法的:bxy(q.push, n, ;)
。
如果参数中含有宏,编译器不会优先进行参数中的宏展开。如果展开式中含有宏,编译器会继续展开它。但是,如果展开式中含有宏且参数中也含有宏,编译器会先展开参数中的宏。
上面一段文字太抽象,我就写一个样例示范一下。
#define macro(x) (1+mmacro(x)) //由于这里专门讲宏所以就不把宏名全大写了
#define mmacro(x) (2+x)
#define macroexpand(x) x
#define expand(x) macroexpand(x)
expand(macro(1)) //macroexpand的参数将是(1+(2+1))
//展开过程如下:
//expand(macro(1))
//macroexpand(macro(1))
//macroexpand((1+mmacro(1)))
//macroexpand((1+(2+1)))
//(1+(2+1))
这些符号是宏独有的功能,其作用相当于直接沟通神灵。他们能改变编译器看到的东西。
单个井号表示将该参数左右加上双引号。
宏被人们所诟病的理由之一就是不能看到宏的展开式进行调试。实际上,只利用我们现在所学的知识,我们是能够看到宏的展开式的。
以下便是一个输出宏展开式的例子
#define macroexpand(x) #x
#define expand(x) macroexpand(x)
//expand函数接受一个宏,返回一个字符串,字符串内容即其展开式
#define expand_andprint(x) printf("%s\n",macroexpand(x))
//expand_andprint函数接受一个宏,将其展开式输出
双井号表示拼接左右两边的内容生成新的合法字面常量或标识符
比如说,我们可以用双井号生成标识符(即变量名):
#define connect(x,y) x##y
expand_andprint(connect(a,b))
//输出 ab
int connect(a,b)=3; //展开为int ab=3
printf("%d",connect(a,b)); //输出3
双井号还能连接生成数字常量。
#define oct(x) 0##x
#define hex(y) 0x##y
hex(7f7f7f7f) //展开为0x7f7f7f7f
#@ 表示将参数加上单引号。
与单井号类似,就不多说了。
注意,这是微软家编译器(VS)专用的符号,不是语言标准内容,在其他编译器上会报错。
如果想用字符,可以使用 #x[0]
或者传入字符参数。
#include <cstdio>
int main()
{
char c;
int a,b;
scanf("%d%c%d",&a,&c,&b);
switch(c)
{
case '+':
printf("%d\n",a+b);
return 0;
case '-':
printf("%d\n",a-b);
return 0;
case '*':
printf("%d\n",a*b);
return 0;
case '/':
printf("%d\n",a/b);
return 0;
}
return 0;
}
#include <cstdio>
using namespace std;
#define charcase(ch,x) case ch: printf("%d\n", a x b); return 0;
int main()
{
char c;
int a,b;
scanf("%d%c%d",&a,&c,&b);
switch(c)
{
charcase('+',+);
charcase('-',-);
charcase('*',*);
charcase('/',/);
}
return 0;
}
在 BFS 走迷宫的时候,经常遇到同样的代码片段出现两次,一次针对 x 一次针对 y。那么有没有办法只写一次呢?
queue<int> xq;
queue<int> yq;
queue<int> q;
q.push(xb);
q.push(yb);
#define xy(a,sy,b) a x##sy b a y##sy b
#define bxy(a,sy,b) a ( x##sy ) b a ( y##sy ) b
xy(queue<int>, q, ;)
queue<int> q;
bxy(q.push, b, ;)
观看本节前,建议阅读参考文献中的 编程利器-lambda表达式。
来个最贴近 OI 的应用。(来源于 编程利器-lambda表达式。)
假设有一道毒瘤题,让你定义一个结构体 people,然后先根据 age 字段排序,然后再根据 chengji 字段排序,最后根据 RP 字段排序。使用 lambda 表达式,我们可以免于写 cmp1、cmp2、cmp3,可以写成这样:
//input
sort(peoples,peoples+n,[](people a,people b){return a.age>b.age;});
//do something
sort(peoples,peoples+n,[](people a,people b){return a.chengji>b.chengji;});
//do something
sort(peoples,peoples+n,[](people a,people b){return a.RP>b.RP;});
//do something
我们发现这三行重复特多,打起来特烦,但是没有多少不同的地方,就可以利用宏做到打一遍抵三遍的效果。
#define arrsort(arr,len,cmp) sort(arr,arr+(len),cmp)
#define stru_op_cmp(stru,field,oper) [](stru a,stru b){return a.field oper b.field;}
//将 stru 的 field 字段按 op 排序的 lambda 表达式
#define sort_people(field) arrsort(peoples,n,stru_op_pre(people,field,>))
sort_people(age);
sort_people(chengji);
sort_people(RP);
以下是更多相似的宏:
#define op_cmp(type,oper) [](type a,type b){return a oper b;}
//返回根据 oper 排序的 lambda 函数,接受两个 type 类型的参数
#define stru_op_cmp(stru,fie,oper) [](stru a,stru b){return a.fie oper b.fie;}
//返回用 stru 的 fie 字段根据 oper 排序的 lambda 函数
#define int_op_cmp(oper) op_pre(int,oper)
//返回根据 oper 排序 int 的 lambda 函数
前面说过宏可以实现 max 函数,然而复杂度是假的。最简陋的宏实现如下:
#define max(a,b) ((a)<(b) ? (b) : (a))
在写线段树的时候,这个 max 可以被卡到单次询问 algorithm
中的 std::max
,但是这不是本文要讲的东西。
首先介绍一个 C++ 中鲜为人知的语法:语句内嵌表达式。这个语法在 C 中就已经存在,所以考试的时候可以放心用。
语句内嵌表达式允许你将一组语句当成一个表达式来用。利用这个语法, max 就可以写成这样:
#define max(a,b) ({ \
int ASDF = (a); \
int FGHJ = (b); \
ASDF<FGHJ ? FGHJ : ASDF; \
})
这还没有解决所有问题。如果用户自己定义了 ASDF
和 FGHJ
,然后调用 max(ASDF,FGHJ)
,这个函数就会炸得很惨。
解决方法 1 类似于这样子:
#define max(a,b) /* DO NOT APPLY THIS MACRO TO ANYTHING NAMED "ASDF" OR "FGHJ" */\
({ \
int ASDF = (a); \
int FGHJ = (b); \
ASDF<FGHJ ? FGHJ : ASDF; \
})
解决方法 2 类似于这样子:
#define max(a,b) \
[](int ASDF,int FGHJ){ \
return ASDF<FGHJ ? FGHJ : ASDF; \
}((a),(b))
解决方法 2 真正解决了问题,但是它需要 C++11,并且在这种场合使用 lambda
有点杀鸡用牛刀。(在 Lisp 等函数式编程语言的体系中,lambda 是一个基础功能,这个解决方法就很优雅。但是在 C++ 的体系中,lambda 需要借助 functor,就很不优雅了。)利用 __COUNTER__
宏,我们可以更优雅地解决这个问题。
__COUNTER__
宏是预处理器定义的一个宏,作用是能在第一次展开的时候展开成 0
,第二次展开的时候展开成 1
,以此类推。那么,我们就可以生成一个不重复的变量名:
#define pas(a,b) a##b
#define paste(a,b) pas(a,b)
#define unique_id(prefix) paste(prefix,paste(_UNIQUE_ID_,__COUNTER__))
#define mmax(a,b,maxa,maxb) \
({ \
int maxa = (a); \
int maxb = (b); \
maxa<maxb ? maxb : maxa; \
})
#define max(a,b) mmax(a,b,unique_id(max),unique_id(max))
然后注意不要让程序里面出现含 _UNIQUE_ID_
的变量名就好,应该还是比较轻松的。
最后还有一个地方可以再优化一下。这个宏只能用于求两个 int 参数的 max,那么如何求任意类型参数的 max?答案是使用 decltype
(需 C++11),或者让用户自己传入类型,不赘述。
#define function(rtype,name,arglis) \
rtype name arglis {
#define endfunction \
}
function(int,main,())
printf("qwq\n");
endfunction
C++ 的宏都是简单的字符串拼接,导致它们只能实现一些很基础的功能。
但是如果 C++ 的宏是一个接受 C++ 代码、返回 C++ 代码的函数呢?
Lisp 的宏就是 Lisp 在编译时运行的程序,能将表达式任意变形成需要的形式。利用宏,甚至可以在 Lisp 中做出内嵌语言,将 Lisp 改造成一个完全不同的形式。比如,在 Lisp 里面使用指针,将面向对象引入 Lisp,或者使用 Brainf**k 的语法。在 Onlisp 中可以简单一览。
由于 Lisp 宏的强大一大部分来源于 Lisp 语法结构(S-expression)的古怪,因此即使是用伪代码,我也很难在 C++ 上将 Lisp 的宏的强大展示给读者。参考文献中 Lisp的本质 一文对此有通俗易懂的论述,有兴趣的读者可以去看看。
感谢以下作者。
本文可任意转载或改编,但须署原作者姓名(笔名)及原文地址,并且应携带此版权信息。