C++中偷懒利器——宏

木木!

2018-08-19 18:19:37

Tech. & Eng.

提到C++宏,大多数人想到的就是宏函数和宏常量,如 #define MAXN 500#define max(a,b) ((a)>(b)?(a):(b)) 这种 会出锅的 宏应用。

其实宏还有很多很多更能发挥它威力的应用。宏的用途可不仅限于 constexpr。几乎任何有重复代码的地方都能用宏大幅度简化,从而节省代码量、提高可复用性。

在继续学习之前,先了解一下 C++ 的宏机制。

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

井号-at号(#@)

#@ 表示将参数加上单引号。

与单井号类似,就不多说了。

注意,这是微软家编译器(VS)专用的符号,不是语言标准内容,在其他编译器上会报错。

如果想用字符,可以使用 #x[0] 或者传入字符参数。

特殊符号的应用

例1:switch-case

原代码:

#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;
}

例2:双井号的使用

在 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表达式

观看本节前,建议阅读参考文献中的 编程利器-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

前面说过宏可以实现 max 函数,然而复杂度是假的。最简陋的宏实现如下:

#define max(a,b) ((a)<(b) ? (b) : (a))

在写线段树的时候,这个 max 可以被卡到单次询问 \Theta(n),连暴力都不如。最简单的方法是使用 algorithm 中的 std::max,但是这不是本文要讲的东西。

首先介绍一个 C++ 中鲜为人知的语法:语句内嵌表达式。这个语法在 C 中就已经存在,所以考试的时候可以放心用。

语句内嵌表达式允许你将一组语句当成一个表达式来用。利用这个语法, max 就可以写成这样:

#define max(a,b) ({          \
    int ASDF = (a);          \
    int FGHJ = (b);          \
    ASDF<FGHJ ? FGHJ : ASDF; \
})

这还没有解决所有问题。如果用户自己定义了 ASDFFGHJ,然后调用 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

拓展:Lisp中的宏

C++ 的宏都是简单的字符串拼接,导致它们只能实现一些很基础的功能。

但是如果 C++ 的宏是一个接受 C++ 代码、返回 C++ 代码的函数呢?

Lisp 的宏就是 Lisp 在编译时运行的程序,能将表达式任意变形成需要的形式。利用宏,甚至可以在 Lisp 中做出内嵌语言,将 Lisp 改造成一个完全不同的形式。比如,在 Lisp 里面使用指针,将面向对象引入 Lisp,或者使用 Brainf**k 的语法。在 Onlisp 中可以简单一览。

由于 Lisp 宏的强大一大部分来源于 Lisp 语法结构(S-expression)的古怪,因此即使是用伪代码,我也很难在 C++ 上将 Lisp 的宏的强大展示给读者。参考文献中 Lisp的本质 一文对此有通俗易懂的论述,有兴趣的读者可以去看看。

参考文献

感谢以下作者。

版权声明

本文可任意转载或改编,但须署原作者姓名(笔名)及原文地址,并且应携带此版权信息。