追本溯源
这期主题是NARG
宏,它被用于计算宏的变长参数的个数。所以这篇文章我想从C语言的变长参数聊起,先说说C语言变长参数应用,以及函数和宏的变长参数的原理,这样在介绍NARG
时,我们不仅能够知道它是如何实现的,还能更好地学以致用。
| 函数的变长参数
变长参数看起来很高级,但当C语言初学者第一次敲下Hello World
时,就已经在不知不觉中使用了这个重要的特性。
printf("Hello World");
printf
函数是一个带变长参数的函数,它的定义如下:
static char buffer[BUFF_SIZE];
int printf(const char *fmt, ...)
{
va_list args;
int n;
va_start(args, fmt);
n = vsprintf(buffer, fmt, args);
va_end(args);
// OUTPUT TO DEVICE
return n;
}
让我们逐行解析这个函数:
-
...
:变长参数形参 -
va_list args
:定义一个指向形参列表的指针 -
va_start(args, fmt)
:根据fmt形参获取函数栈帧中可变参数列表的地址,并初始化args
当调用
printf
函数时,其函数栈帧中包含形参列表,完整函数栈帧如下:
局部变量【栈顶】
形参1
...
形参n
函数返回地址
相关寄存器备份
函数返回值【栈底】
-
vsprintf
:将args
变长参数列表按照fmt
字符串中的规则解析,并将结果填到buffer
中,返回值为填入buffer
的字节数
拓展知识:
vsprintf
也可以使用安全函数vsnprintf
,后者多一个表示buffer大小的参数。
在嵌入式环境中,通常在OUTPUT TO DEVICE
位置会将buffer中数据输出到串口或者其他输出设备;在带文件系统的操作系统中,vsprintf
函数会被vfprintf
取代,数据输出结果不填入buffer,而是填入stdout
标准输出流中(如命令行程序打印到屏幕上)
-
va_end(args)
:将args
置为NULL
细心的读者可能会提出疑问,printf
函数使用变长参数时,为何不需要知道变长参数的个数呢?
其实C语言中函数的变长参数个数是需要通过入参传入的,函数的栈帧中也确实不会自动生成变长参数个数的信息,只不过printf
函数的入参fmt
中已间接包含了变长参数个数信息,即有多少个%
。
如果要实现一个不定长参数的my_sum
求和函数,那就只能多加一个代表变长参数个数的入参了:
// 约束:变长入参为int类型
int my_sum(int num, ...)
{
va_list args;
int sum = 0;
va_start(args, num);
while (num--) {
sum += va_arg(args, int);
}
va_end(args);
return sum;
}
// 调用
int sum = my_sum(3, 1, 2, 3); // sum = 6
但是,有没有办法优化这个my_sum
函数,让程序在编译时计算
变长参数个数,而非手动填写呢?
| 宏的变长参数
与函数的变长参数不同的是,宏只作用在预编译阶段,即对宏的变长参数的操作在代码运行之前,不会造成运行时开销。
下面是使用NARG
宏优化后的my_sum
函数:
#define ARG_N(_0, _1, _2, _3, _4, _5, ...) _5
#define NARG(...) ARG_N(__VA_ARGS__, 5, 4, 3, 2, 1, 0)
#define MY_SUM(...) my_sum(NARG(__VA_ARGS__), ...)
// 调用
int sum = MY_SUM(1, 2, 3); // sum = 6
我们来逐行解析MY_SUM
的实现:
-
ARG_N
:定义一个含N+1
个固定参数以及最后一个为变长参数的宏,其值为最后一个固定参数_N
(这里N取5,最大支持计算5个参数数量) -
NARG
:利用ARG_N
值为最后一个固定参数的特性,将变长参数放参数列表开头,后续参数为从N
到0
递减,巧妙地让变长参数个数与ARG_N
宏的值相等 -
MY_SUM
:利用NARG
在预编译时计算出参数个数,填入my_sum
函数第一个参数
宏的变长参数在使用时有2种方式:
1、__VA_ARGS__
:可以不搭配固定参数使用,但变长参数个数需大于0
2、##__VA_ARGS__
:必须搭配至少一个固定参数使用,且变长参数个数可为0
以下为__VA_ARGS__
和##__VA_ARGS__
合法和非法的使用方式:
#define MACRO1(...) func(__VA_ARGS__)
#define MACRO2(arg, ...) func(__VA_ARGS__)
#define MACRO3(arg, ...) func(##__VA_ARGS__)
// 合法
MACRO1(1)
MACRO2(1,2)
MACRO3(1)
MACRO3(1,2)
// 非法
MACRO1()
MACRO2(1)
实际应用
开源项目googletest
中的googlemock
就使用了NARG
,以及除此之外的超多宏魔法
,这是由于其需要对外部输入做很多的编译时合法性检查。在后续的宏魔法系列文章中,可能会再次引用googlemock
中的实现思路。
更进一步
上述MY_SUM
宏通过调用my_sum
这个带变长参数函数实现了不定个数数字求和。假如MY_SUM
宏的参数均为常量,有没办法继续优化,使程序完全在编译时计算
呢?
#define SUM1(a1) (a1)
#define SUM2(a1, a2) (SUM1(a1) + (a2))
#define SUM3(a1, a2, a3) (SUM2(a1, a2) + (a3))
#define SUM4(a1, a2, a3, a4) (SUM3(a1, a2, a3) + (a4))
#define SUM_N(_1, _2, _3, _4, NAME, ...) NAME
#define MY_SUM(...) SUM_N(__VA_ARGS__, SUM4, SUM3, SUM2, SUM1)(__VA_ARGS__)
// 调用
int sum = MY_SUM(1, 2, 3); // sum = 6
以上MY_SUM
实现原理留给读者思考,其原理与NARG
宏如出一辙。
原理:
将一个前为固定参数,后为变长参数的前定后变
宏,传入一个前为变长参数,后为固定参数的前变后定
参数列表,利用前定后变
宏固定参数位置不变的性质,按照规则排列好前变后定
参数列表,完成参数列表的选择。