C++中偷懒利器——宏

木木!

2018-08-19 18:19:37

Personal

提到C++宏,大多数人想到的就是宏函数和宏常量,如 `#define MAXN 500` 和 `#define max(a,b) ((a)>(b)?(a):(b))` 这种 ~~会出锅的~~ 宏应用。 其实宏还有很多很多更能发挥它威力的应用。宏的用途可不仅限于 `constexpr`。几乎任何有重复代码的地方都能用宏大幅度简化,从而节省代码量、提高可复用性。 在继续学习之前,先了解一下 C++ 的宏机制。 # C++的宏机制 (图片来源于网络,侵权删。) ![](https://cdn.luogu.com.cn/upload/image_hosting/1kub3c06.png) C++的编译流程主要分为预处理、编译、汇编、链接等步骤,其中宏展开在预处理步骤中进行。预处理步骤主要处理预处理指令,就是最前面有引号的指令,包括宏所用的 `#define` 以及头文件用的 `#include` 等。 因此,宏在预处理过程中就全部展开。这时候编译器只进行了词法分析(将输入源程序中的数字标识符等识别出来),没有进行语法分析等,因此预处理器执行的只是简单的字符串替换。 具体到每个宏,预处理器若识别出一个符号为宏名,就执行宏展开。对于每一个宏名后面的括号里的内容,预处理器根据且只根据逗号分割参数。如果有括号,括号内的逗号不会被当作分割符。也就是说这样的宏调用是合法的:`bxy(q.push, n, ;)`。 如果参数中含有宏,编译器不会优先进行参数中的宏展开。如果展开式中含有宏,编译器会继续展开它。但是,如果展开式中含有宏且参数中也含有宏,编译器会先展开参数中的宏。 上面一段文字太抽象,我就写一个样例示范一下。 ```cpp #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)) ``` # 宏中的特殊符号 这些符号是宏独有的功能,其作用相当于直接沟通神灵。他们能改变编译器看到的东西。 ## 井号(\#) 单个井号表示将该参数左右加上双引号。 宏被人们所诟病的理由之一就是不能看到宏的展开式进行调试。实际上,只利用我们现在所学的知识,我们是能够看到宏的展开式的。 以下便是一个输出宏展开式的例子 ```cpp #define macroexpand(x) #x #define expand(x) macroexpand(x) //expand函数接受一个宏,返回一个字符串,字符串内容即其展开式 #define expand_andprint(x) printf("%s\n",macroexpand(x)) //expand_andprint函数接受一个宏,将其展开式输出 ``` ## 双井号(\#\#) 双井号表示拼接左右两边的内容生成新的**合法字面常量或标识符** 比如说,我们可以用双井号生成标识符(即变量名): ```cpp #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 ``` 双井号还能连接生成数字常量。 ```cpp #define oct(x) 0##x #define hex(y) 0x##y hex(7f7f7f7f) //展开为0x7f7f7f7f ``` ## 井号-at号(\#@) \#@ 表示将参数加上单引号。 与单井号类似,就不多说了。 注意,这是微软家编译器(VS)专用的符号,不是语言标准内容,在其他编译器上会报错。 如果想用字符,可以使用 `#x[0]` 或者传入字符参数。 # 特殊符号的应用 ## 例1:switch-case ### 原代码: ```cpp #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; } ``` ### 加入宏之后 ```cpp #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; } ``` ## 例2:双井号的使用 在 BFS 走迷宫的时候,经常遇到同样的代码片段出现两次,一次针对 x 一次针对 y。那么有没有办法只写一次呢? ### 原代码(代码片段) ```cpp queue<int> xq; queue<int> yq; queue<int> q; q.push(xb); q.push(yb); ``` ### 加入宏之后(等效代码片段) ```cpp #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表达式 观看本节前,建议阅读参考文献中的 [编程利器-lambda表达式](https://www.luogu.org/blog/64456/bian-cheng-li-qi-lambda-biao-da-shi)。 来个最贴近 OI 的应用。(来源于 [编程利器-lambda表达式](https://www.luogu.org/blog/64456/bian-cheng-li-qi-lambda-biao-da-shi)。) 假设有一道毒瘤题,让你定义一个结构体 people,然后先根据 age 字段排序,然后再根据 chengji 字段排序,最后根据 RP 字段排序。使用 lambda 表达式,我们可以免于写 cmp1、cmp2、cmp3,可以写成这样: ```cpp //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 ``` 我们发现这三行重复特多,打起来特烦,但是没有多少不同的地方,就可以利用宏做到打一遍抵三遍的效果。 ```cpp #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); ``` 以下是更多相似的宏: ```cpp #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 前面说过宏可以实现 max 函数,然而复杂度是假的。最简陋的宏实现如下: ```cpp #define max(a,b) ((a)<(b) ? (b) : (a)) ``` 在写线段树的时候,这个 max 可以被卡到单次询问 $\Theta(n)$,连暴力都不如。最简单的方法是使用 `algorithm` 中的 `std::max`,但是这不是本文要讲的东西。 首先介绍一个 C++ 中鲜为人知的语法:语句内嵌表达式。这个语法在 C 中就已经存在,所以考试的时候可以放心用。 语句内嵌表达式允许你将一组语句当成一个表达式来用。利用这个语法, max 就可以写成这样: ```cpp #define max(a,b) ({ \ int ASDF = (a); \ int FGHJ = (b); \ ASDF<FGHJ ? FGHJ : ASDF; \ }) ``` 这还没有解决所有问题。如果用户自己定义了 `ASDF` 和 `FGHJ`,然后调用 `max(ASDF,FGHJ)`,这个函数就会炸得很惨。 解决方法 1 类似于这样子: ```cpp #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 类似于这样子: ```cpp #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`,以此类推。那么,我们就可以生成一个不重复的变量名: ```cpp #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),或者让用户自己传入类型,不赘述。 # 用宏避免圣战 ```cpp #define function(rtype,name,arglis) \ rtype name arglis { #define endfunction \ } function(int,main,()) printf("qwq\n"); endfunction ``` # 拓展:Lisp中的宏 C++ 的宏都是简单的字符串拼接,导致它们只能实现一些很基础的功能。 但是如果 C++ 的宏是一个接受 C++ 代码、返回 C++ 代码的函数呢? Lisp 的宏就是 Lisp 在编译时运行的程序,能将表达式任意变形成需要的形式。利用宏,甚至可以在 Lisp 中做出内嵌语言,将 Lisp 改造成一个完全不同的形式。比如,在 Lisp 里面使用指针,将面向对象引入 Lisp,或者使用 Brainf\*\*k 的语法。在 [Onlisp](https://www.kancloud.cn/ituring/on-lisp/56193) 中可以简单一览。 由于 Lisp 宏的强大一大部分来源于 Lisp 语法结构(S-expression)的古怪,因此即使是用伪代码,我也很难在 C++ 上将 Lisp 的宏的强大展示给读者。参考文献中 [Lisp的本质](https://blog.csdn.net/d603010999/article/details/48155837) 一文对此有通俗易懂的论述,有兴趣的读者可以去看看。 # 参考文献 感谢以下作者。 + [C++编译过程简介 -云东](https://www.cnblogs.com/dongdongweiwu/p/4743709.html) + [C++宏定义详解 -dongfs_love](http://blog.chinaunix.net/uid-21372424-id-119797.html) + [编程利器——lambda表达式 -colazcy](https://www.luogu.org/blog/64456/bian-cheng-li-qi-lambda-biao-da-shi) + [Lisp的本质 -Slava Akhmechet,译者 Alec Jang](https://blog.csdn.net/d603010999/article/details/48155837) + [OnLisp -Paul Graham,译者田春](https://www.kancloud.cn/ituring/on-lisp/56192) # 版权声明 本文可任意转载或改编,但须署原作者姓名(笔名)及原文地址,并且应携带此版权信息。