可变参数模板

**可變參數模板**是模板編程時，模板參數（template parameter）的個數可變的情形。

已經支持可變參數模板的程式語言有 D 語言與 C++（自 C++11 標準）。

## C++11

### 聲明

C++11 之前，模板（類模板與函數模板）在聲明時必須有 固定數量的模板參數。C++11 允許模板定義有任意類型任意數量的模板參數。

例如，STL 的類模板 tuple 可以有任意個數的類型名（typename）作為它的模板形參（template parameter）：

如實例化為具有 3 個類型實參（type argument）：

也可以有 0 個實參，如 `tuple<> some_instance_name;` 也是可以的。

如果不希望可變參數模板有 0 個模板實參，可以如下聲明：

可變參數模板也適用於函數模板，這不僅給可變參數函數（variadic functions，如 printf）提供了類型安全的附加機制（add-on），還允許類似 printf 的函數處理不平凡對象。例如：

### 用途

省略號 (...) 在可變參數模板中有兩種用途：

* 省略號出現在形參名字左側，聲明了一個**參數包**（parameter pack）。使用這個參數包，可以綁定 0 個或多個模板實參給這個模板參數包。參數包也可以用於非類型的模板參數。
* 省略號出現在包含參數包的表達式的右側，稱作**參數包展開**（parameter pack expansion），是把這個參數包解開為一組實參，使得在省略號前的整個表達式使用每個被解開的實參完成求值，所有表達式求值結果被逗號分開。這種表達式必須是可接受任意個數的以逗號分開的子表達式。注意這裡的逗號不是作為逗號運算符，而是用作：
  * 被逗號分隔開的一組函數調用實參列表；（該函數必須是可變參數函數，而不能是固定參數個數的函數）
  * 被逗號分隔開的一組初始化器列表（initializer list）；
  * 被逗號分隔開的一組基類列表（base class list）與構造函數初始化列表（constructor's initialization list）；
  * 被逗號分隔開的一組函數的可拋出的異常規範（exception specification）的聲明列表。
  * Lambda 表達式的被逗號分隔開的用一對方括號包圍的捕獲。
  * 類模板或者函數模板定義中，
    * 類模板實參如果也是類模板並需要模板參數包作為實參，如 template<unsigned... I1, unsigned... I2>struct concat<seq<I1...>, seq<I2...>>
    * 類模板的基類如果也是類模板，則其模板實參列表是逗號分隔的一個表達式，如：template<unsigned... I1, unsigned... I2>struct concat: seq<I1..., (sizeof...(I1) + I2)...>

具體例子見下文。實際上，能夠接受可變參數個數的參數包展開的場合，必須是能接受任意個數的逗號分隔開的表達式列表，這也就是上述幾種場合。

如果在一個參數包展開中同時出現了兩個參數包的名字，則二者在一起同時展開，且應該具有相同長度。這可能出現在類模板帶有一個參數包，它的嵌套的類模板或成員函數模板帶有另一個參數包。

### 使用方法

可變參數模板可遞歸使用。可變模板參數自身並不可直接用於函數或類的實現。例如，printf 的 C++11 可變參數的替換版本實現：

這是一個遞歸實現的模板函數。注意這個可變參數模板實現的 printf 調用自身或者在 args... 為空時調用基本實現版本。

沒有簡單機制去在可變模板參數的每個單獨值上迭代。幾乎沒有什麼方式可以把參數包轉為單獨實參來使用。通常這靠函數重載，或者當函數可以每次撿出一個實參時用啞擴展標記（dumb expansion marker）：

上例中的 "pass" 函數是必須的，因為參數包用逗號展開後只能作為被逗號分隔開的一組函數調用實參，而不是作為逗號運算符，從而 "pass" 函數所能接受的調用實參個數必須是可變的，也即 "pass" 函數必須是可變參數函數。`print(args)...;` 編譯不能通過。 此外，上述辦法要求 `print` 的返回類型不能是 void；且所有對 print 的調用在一個非確定的順序，因為函數實參求值的順序是不確定的。如果要避免這種不確定的順序，可以用大括號封閉的初始化器列表（initializer list），這保證了嚴格的從左到右的求值順序。為避免 void 返回類型帶來的麻煩，使用逗號運算符使得每個擴展元素總是返回 1。例如：

另一種方法使用重載函數的遞歸的終結版（"termination versions"）函數。這更為通用，但要求更多努力寫更多代碼。一個函數要求某種類型的實參與參數包。另一個函數沒有參數。如下例：

如果 args... 包含至少一個實參，則將調用第二個版本的函數；如果參數包為空將調用第一個「終結」版的函數。

可變參數模板可用於異常規範（exception specification）、基類列表（base class list）、構造函數初始化列表（constructor's initialization list）。例如：

這個例子中的解包算子將複製所有模板參數類型為 `ClassName` 的基類型。構造函數取每個基類的引用，並初始化每個基類。

對於函數模板，可變模板參數可以轉發（forward）。當與右值引用結合使用，這允許完美轉發（perfect forwarding）：

上例中，實參列表被解包給 TypeToConstruct 的構造函數。`std::forward<Args>(params)` 的句法是以適當的類型轉發實參。解包算子將把轉發語法應用於每個參數。

模板參數包中實參的個數可以如下確定：

例如 `SomeStruct<Type1, Type2>::size` 為 2，`SomeStruct<>::size` 為 0。需要注意，`sizeof...` 與 `sizeof` 是兩個不同的運算符。

Lambda 捕獲例子：

### 編譯器實現

GCC 尚不支持 lambda 表達式包含為展開的參數包，因此下述語句編譯不通過：

```
   int arr[]{([&]{ std::cout << args << std::endl; }(), 1)...};
```

Visual C++ 2013 支持上述風格的語句。當然，這裡的 lambda 函數不是必需的，通常的表達式即可：

```
  int arr[]{(std::cout << args << std::endl, 1)...};
```

## 例子

下述代碼實現了 C++14 引入的 make\_integer\_sequence 函數模板。它產生一個模板類，其模板參數為 0,1,2,...,N。可用於生成或訪問 std::tuple

輸出為

```
0 1 2 3 4 5 6 7 8 9
```

## 參見

更多文章關於可變參數結構而非模板：

* 可變參數函數
* 可變參數宏

## 參考文獻

## 外部連結

* Working draft for the C++ language, January 16, 2012（頁面存檔備份，存於網際網路檔案館）
* Variadic Templates in D language （頁面存檔備份，存於網際網路檔案館）