C++ 實現可變參數的三個方法
- 2022 年 7 月 27 日
- 筆記
有時我們無法提前預知應該向函數傳遞幾個實參。例如,我們想要編寫程式碼輸出程式產生的錯誤資訊,此時最好用同一個函數實現該項功能,以便對所有錯誤的處理能夠整齊劃一。然而,錯誤資訊的種類不同,所以調用錯誤輸出函數時傳遞的實參也各不相同。編寫變長參數的函數有幾種方法:
C方法:va_list
stdarg.h頭文件提供了C語言中變長參數的功能,但用法較為複雜。
要使用C方法變長參數,首先在函數聲明裡用...聲明列表(必須在最後,且不能只有...):
int f(int n, ...);//合法
int g(int n, ... int m);//不合法
int h(...)//不合法
在調用的時候,我們就可以傳入變長參數:f(3, 400, 500, 600);
然後在函數定義里,聲明va_list類型的變數(隨意取名):va_list myargs;
然後使用va_start將變長參數拷貝進來,(第二個參數通常是最後一個具名參數,代表長度):va_start(myargs, n);
接下來,我們可以不斷調用va_arg獲取下一個參數:
va_arg(myargs, int); //獲取下一個int
va_arg(myargs, double); //獲取下一個double
最後使用va_end清理結尾:va_end(myarg)
完整程式碼:
#include <stdarg.h>
int f(int n,...) {
va_list myarg;
va_start(myarg, 10);
int ans(0);
for (int i(0);i<n;i++) ans += va_arg(myarg, int); //僅支援int
va_end(myarg);
return ans;
}
從中就可以看出,這個變長參數列表是直接以獲取二進位流,沒有包含類型資訊,所以做不到處理各種類型。上面的函數只能在傳入32位數時工作,傳入64位數或者浮點數就會截斷成兩個數再相加。所以C中的scanf printf兩個函數必須傳入格式化字元串指定類型,並且不能寫錯,不然就會有錯誤的結果。
C++方法:使用initializer_list
initializer_list傳參則要簡單的多,首先這個東西自己就是一種模板容器,使用方法和vector類似。

不過,它的元素值只作為參數傳遞,通常不能修改。有了這個東西,我們就可以把多個參數打包成一個參數傳入.
#include <initializer_list>
int max(std::initializer_list<int> li) {
int ans = 1 << 31;
for (auto x: li) ans = ans>x ? ans : x;
return ans;
}
main() {
printf("%d\n", max({1, 2, 3})); //加上大括弧,作為整體調用
}
優點:很靈活,易於理解。
缺點:
- 只支援單一類型
- 只能讀,不能寫
- 要額外加括弧,形式不統一
其實C++11以後標準庫就有了變長max了,就是使用的這種括弧形式。
C++方法:使用可變參數模板
一個可變參數模板(variadic template)就是一個接受可變數目參數的模板函數或模板類。可變數目的參數被稱為參數包(parameter packet)。存在兩種參數包:模板參數包(template parameter packet),表示零個或多個模板參數;函數參數包function parameterpacket),表示零個或多個函數參數。
用下面的方式聲明一個可變參數模板:
template<class T, class... Args> //Args:「模板參數包」
void foo(const T &t, const Args&... test); //test:「一個參數包(含有0或多個參數)」
foo(i, s, 42, d); //包中有三個參數
foo(s, 42, "hi"); //包中有兩個參數
foo(d, s); //包中有一個參數
foo("hi"); //空包
如此,編譯器會在編譯器就實例化所有出現過的版本:
void foo(const int&,const string&,const int&,const double& );
void foo(const string&,const int&, const char [3]&);
void foo(const double&, const string&);
void foo(const char[3] &);
模板通常採用遞歸方式展開:
template<class T>
void print(T &t) {cout << t <<'\n';}
template<class T, class... Args>
void print(T &t, Args&... rest) {
cout << t << ' ';
print(rest...); // 列印剩餘參數,注意省略號必須有
}
在調用print時,首先調用變長版,然後遞歸調用print,直到剩下最後一個參數。由於Args也能使用零參數,就與第一個函數形成重載,此時會重載第一個更特化函數,結束遞歸。所以必須有第一個函數,否則會無限遞歸。
優點:
- 功能最強大,這裡只列出入門知識,模板參數包還有非常多的用法。
- 編譯時展開,效率高。
缺點:
- 語法複雜繁瑣,語義不清晰。省略號的位置我現在都記不清只能現查。更別說進階語法。
- 更多是一種炫技。實際應用只需要最基本形式的變長模板就夠了。

