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