预编译变参系统与预编译器粘结运算符以及"回调宏"的应用

C/C++

Posted by Bruce Lee on 2024-05-10

预编译变参系统与预编译器粘结运算符以及"回调宏"的应用

使用变参系统时,无预编译器粘结运算符的情况

这里提到的变参系统不是stdarg文件中声明的那些系列函数与变量系统.而是针对用于宏定义中的变参系统

在有些情况下,我们在使用__VA_ARGS__时,是不需要添加前缀##.

关于前缀##(预编译器粘结运算符),见这篇C编译器名称粘结运算符

__VA_ARGS__是预编译器宏, 在预编译阶段的参数替换时,替换发生在宏展开后的出现__VA_ARGS__的地方.

正常的使用这个预编译器宏:

1
2
3
4
5
6
7
#include <stdio.h>
#define MESSAGE_OUTPUT(msg, ...) printf(msg, __VA_ARGS__)
int main(int argc, char** argv)
{
MESSAGE_OUTPUT("File: %s, Function: %s, Line: %d\n", __FILE__, __func__, __LINE__);
return 0;
}

经过预编译后的代码:

1
2
3
4
5
6
7
# 4 "test3.c"
int main(int argc, char** argv)
{
printf("File: %s, Function: %s, Line: %d\n", "test3.c", __func__, 6);
return 0;
}

这里有很好的,意料之中的结果.正如我们经常做的那些动作.

如果这个定义的MESSAGE_OUTPUT宏,不仅仅是用来定位调用MESSAGE_OUTPUT宏的位置,也可以用来输出一些注释或者信息.

比如:

1
MESSAGE_OUTPUT("This function does some things");

这样编写程序:

1
2
3
4
5
6
7
8
#include <stdio.h>
#define MESSAGE_OUTPUT(msg, ...) printf(msg, __VA_ARGS__)
int main(int argc, char** argv)
{
MESSAGE_OUTPUT("This function does some things");
return 0;
}

经过编译之后,报错:

1
2
3
4
5
6
7
test3.c: In functionmain’:
test3.c:2:57: error: expected expression before ‘)’ token
2 | #define MESSAGE_OUTPUT(msg, ...) printf(msg, __VA_ARGS__)
| ^
test3.c:7:9: note: in expansion of macroMESSAGE_OUTPUT
5 | MESSAGE_OUTPUT("This function does some things");
|

我们在调用MESSAGE_OUTPUT时,除了传入msg之外,没有传入更多的可变参数.这意味着,__VA_ARGS__占位符在预编译阶段被置换为空.

看看预编译文件:

1
2
3
4
5
6
# 4 "test3.c"
int main(int argc, char** argv)
{
printf("This function does some things", );
return 0;
}

这就一目了然了,因为printf函数中多了一个逗号.所以报错提示中显示在")"前面缺少一个表达式.

这就说明,在定义可变参宏时,如果没有使用添加前缀##的__VA_ARGS__,那么很有可能会出现调用宏时的错误.正如上述.

但是,一般而言,我们定义一个宏(拥有指定的作用),一般都会传入除了固定参数以外的参数,

添加预编译器粘结运算符后的情况

程序变成了这样:

1
2
3
4
5
6
7
#include <stdio.h>
#define MESSAGE_OUTPUT(msg, ...) printf(msg, ##__VA_ARGS__)
int main(int argc, char** argv)
{
MESSAGE_OUTPUT("This function does some things");
return 0;
}

只给前者出错的代码示例,其他情况都是一样的
预编译之后:

1
2
3
4
5
6
# 4 "test3.c"
int main(int argc, char** argv)
{
printf("This function does some things");
return 0;
}

printf函数后面的逗号消失了.

这就是’##'的神力了,如果__VA_ARGS__是空的,并且##的前面是逗号,那么预编译器会省略掉这个逗号,防止出现前者那样的错误.

再次强调的一点就是:##和__VA_ARGS__都是只能在宏定义中使用.因为这是预定义的宏,是专门设计用于宏定义中.

它不是传递参数的特权,也可以传递代码块(就像回调函数那样,但并不完全是那样的)

回调函数不陌生,那么如何在宏代码中的合适位置,去调用别家的宏函数呢

我在项目工程中遇到过这样需求的代码,大致是这样的:使用大量的宏包装,简化逻辑和解耦合功能代码.在宏中代替了回调函数,使用变参系统来满足调用别的宏代码块的需求.

我无法直接呈现源码作为例子来编译,运行给大家看,但是我编写了一个极其简陋的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#define PLUS(x, y) (x = ((x) + (y)));
#define SUB(x, y) (x = ((x) - (y)));
#define DO_SOMETHINGS(msg, ...) do{ \
printf(msg); \
/*do more things*/ \
__VA_ARGS__ \
} while(0)
int x = 5;
int y = 3;
int main(int argc, char** argv)
{
DO_SOMETHINGS("Do Plus\n", PLUS(x, y));
printf("x = %d, y = %d\n", x, y);
DO_SOMETHINGS("Do Sub\n", SUB(x, y));
printf("x = %d, y = %d\n", x, y);
return 0;
}

我的DO_SOMETHINGS宏可以做任何想做的事(通过传入其他的宏代码块),这样就避免了愚蠢的在宏的参数里面写下DO_SOMETHINGS("Do Plus\n", PLUS);

像使用真实的C函数那样传入函数地址似的愚笨的想法.

这里使用do{}while(0)是为了将整个宏完备的放在一个代码块中,也可以不使用这个办法(可以直接使用{}来做同样的事)

在DO_SOMETHINGS定义中,我将传入的宏函数(就是__VA_ARGS__这个占位符),放在了合适的位置,让它去做我需要它做的事.

这个示例代码经过编译是完全按照逻辑那样运行的,但是我们更应该看看经过预编译之后,那些宏都变成了什么

1
2
3
4
5
6
7
8
9
10
11
# 9 "test4.c"
int x = 5;
int y = 3;
int main(int argc, char** argv)
{
do{ printf("Do Plus\n"); (x = ((x) + (y))); } while(0);
printf("x = %d, y = %d\n", x, y);
do{ printf("Do Sub\n"); (x = ((x) - (y))); } while(0);
printf("x = %d, y = %d\n", x, y);
return 0;
}

我们所要做的,就是这个test4.i文件的代码所呈现的面貌.在不同的代码块中,使用了不同的代码功能(或加法或减法)

依据这个,我们就可以写出很有用的宏代码.


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !