追本溯源
這期主題是NARG
宏,它被用于計(jì)算宏的變長(zhǎng)參數(shù)的個(gè)數(shù)。所以這篇文章我想從C語(yǔ)言的變長(zhǎng)參數(shù)聊起,先說(shuō)說(shuō)C語(yǔ)言變長(zhǎng)參數(shù)應(yīng)用,以及函數(shù)和宏的變長(zhǎng)參數(shù)的原理,這樣在介紹NARG
時(shí),我們不僅能夠知道它是如何實(shí)現(xiàn)的,還能更好地學(xué)以致用。
| 函數(shù)的變長(zhǎng)參數(shù)
變長(zhǎng)參數(shù)看起來(lái)很高級(jí),但當(dāng)C語(yǔ)言初學(xué)者第一次敲下Hello World
時(shí),就已經(jīng)在不知不覺(jué)中使用了這個(gè)重要的特性。
printf("Hello World");
printf
函數(shù)是一個(gè)帶變長(zhǎng)參數(shù)的函數(shù),它的定義如下:
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;
}
讓我們逐行解析這個(gè)函數(shù):
-
...
:變長(zhǎng)參數(shù)形參 -
va_list args
:定義一個(gè)指向形參列表的指針 -
va_start(args, fmt)
:根據(jù)fmt形參獲取函數(shù)棧幀中可變參數(shù)列表的地址,并初始化args
當(dāng)調(diào)用
printf
函數(shù)時(shí),其函數(shù)棧幀中包含形參列表,完整函數(shù)棧幀如下:
局部變量【棧頂】
形參1
...
形參n
函數(shù)返回地址
相關(guān)寄存器備份
函數(shù)返回值【棧底】
-
vsprintf
:將args
變長(zhǎng)參數(shù)列表按照fmt
字符串中的規(guī)則解析,并將結(jié)果填到buffer
中,返回值為填入buffer
的字節(jié)數(shù)
拓展知識(shí):
vsprintf
也可以使用安全函數(shù)vsnprintf
,后者多一個(gè)表示buffer大小的參數(shù)。
在嵌入式環(huán)境中,通常在OUTPUT TO DEVICE
位置會(huì)將buffer中數(shù)據(jù)輸出到串口或者其他輸出設(shè)備;在帶文件系統(tǒng)的操作系統(tǒng)中,vsprintf
函數(shù)會(huì)被vfprintf
取代,數(shù)據(jù)輸出結(jié)果不填入buffer,而是填入stdout
標(biāo)準(zhǔn)輸出流中(如命令行程序打印到屏幕上)
-
va_end(args)
:將args
置為NULL
細(xì)心的讀者可能會(huì)提出疑問(wèn),printf
函數(shù)使用變長(zhǎng)參數(shù)時(shí),為何不需要知道變長(zhǎng)參數(shù)的個(gè)數(shù)呢?
其實(shí)C語(yǔ)言中函數(shù)的變長(zhǎng)參數(shù)個(gè)數(shù)是需要通過(guò)入?yún)魅氲?,函?shù)的棧幀中也確實(shí)不會(huì)自動(dòng)生成變長(zhǎng)參數(shù)個(gè)數(shù)的信息,只不過(guò)printf
函數(shù)的入?yún)?span style="color: #ff9900;">fmt
中已間接包含了變長(zhǎng)參數(shù)個(gè)數(shù)信息,即有多少個(gè)%
。
如果要實(shí)現(xiàn)一個(gè)不定長(zhǎng)參數(shù)的my_sum
求和函數(shù),那就只能多加一個(gè)代表變長(zhǎng)參數(shù)個(gè)數(shù)的入?yún)⒘耍?/p>
// 約束:變長(zhǎng)入?yún)閕nt類型
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;
}
// 調(diào)用
int sum = my_sum(3, 1, 2, 3); // sum = 6
但是,有沒(méi)有辦法優(yōu)化這個(gè)my_sum
函數(shù),讓程序在編譯時(shí)計(jì)算
變長(zhǎng)參數(shù)個(gè)數(shù),而非手動(dòng)填寫呢?
| 宏的變長(zhǎng)參數(shù)
與函數(shù)的變長(zhǎng)參數(shù)不同的是,宏只作用在預(yù)編譯階段,即對(duì)宏的變長(zhǎng)參數(shù)的操作在代碼運(yùn)行之前,不會(huì)造成運(yùn)行時(shí)開(kāi)銷。
下面是使用NARG
宏優(yōu)化后的my_sum
函數(shù):
#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__), ...)
// 調(diào)用
int sum = MY_SUM(1, 2, 3); // sum = 6
我們來(lái)逐行解析MY_SUM
的實(shí)現(xiàn):
-
ARG_N
:定義一個(gè)含N+1
個(gè)固定參數(shù)以及最后一個(gè)為變長(zhǎng)參數(shù)的宏,其值為最后一個(gè)固定參數(shù)_N
(這里N取5,最大支持計(jì)算5個(gè)參數(shù)數(shù)量) -
NARG
:利用ARG_N
值為最后一個(gè)固定參數(shù)的特性,將變長(zhǎng)參數(shù)放參數(shù)列表開(kāi)頭,后續(xù)參數(shù)為從N
到0
遞減,巧妙地讓變長(zhǎng)參數(shù)個(gè)數(shù)與ARG_N
宏的值相等 -
MY_SUM
:利用NARG
在預(yù)編譯時(shí)計(jì)算出參數(shù)個(gè)數(shù),填入my_sum
函數(shù)第一個(gè)參數(shù)
宏的變長(zhǎng)參數(shù)在使用時(shí)有2種方式:
1、__VA_ARGS__
:可以不搭配固定參數(shù)使用,但變長(zhǎng)參數(shù)個(gè)數(shù)需大于0
2、##__VA_ARGS__
:必須搭配至少一個(gè)固定參數(shù)使用,且變長(zhǎng)參數(shù)個(gè)數(shù)可為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)
實(shí)際應(yīng)用
開(kāi)源項(xiàng)目googletest
中的googlemock
就使用了NARG
,以及除此之外的超多宏魔法
,這是由于其需要對(duì)外部輸入做很多的編譯時(shí)合法性檢查。在后續(xù)的宏魔法系列文章中,可能會(huì)再次引用googlemock
中的實(shí)現(xiàn)思路。
更進(jìn)一步
上述MY_SUM
宏通過(guò)調(diào)用my_sum
這個(gè)帶變長(zhǎng)參數(shù)函數(shù)實(shí)現(xiàn)了不定個(gè)數(shù)數(shù)字求和。假如MY_SUM
宏的參數(shù)均為常量,有沒(méi)辦法繼續(xù)優(yōu)化,使程序完全在編譯時(shí)計(jì)算
呢?
#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__)
// 調(diào)用
int sum = MY_SUM(1, 2, 3); // sum = 6
以上MY_SUM
實(shí)現(xiàn)原理留給讀者思考,其原理與NARG
宏如出一轍。
原理:
將一個(gè)前為固定參數(shù),后為變長(zhǎng)參數(shù)的前定后變
宏,傳入一個(gè)前為變長(zhǎng)參數(shù),后為固定參數(shù)的前變后定
參數(shù)列表,利用前定后變
宏固定參數(shù)位置不變的性質(zhì),按照規(guī)則排列好前變后定
參數(shù)列表,完成參數(shù)列表的選擇。