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類似。
image

不過,它的元素值只作為參數傳遞,通常不能修改。有了這個東西,我們就可以把多個參數打包成一個參數傳入.

#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也能使用零參數,就與第一個函數形成重載,此時會重載第一個更特化函數,結束遞歸。所以必須有第一個函數,否則會無限遞歸。

優點:

  • 功能最強大,這裡只列出入門知識,模板參數包還有非常多的用法。
  • 編譯時展開,效率高。

缺點:

  • 語法複雜繁瑣,語義不清晰。省略號的位置我現在都記不清只能現查。更別說進階語法。
  • 更多是一種炫技。實際應用只需要最基本形式的變長模板就夠了。
Tags: