(C++11 起)

出自cppreference.com


 
 
C++ 語言
 
 
 
 

包是一種 C++ 實體,它定義以下各項之一:

  • 形參包
  • 模板形參包
  • 函數形參包
(C++20 起)
(C++26 起)

模板形參包是接受零個或更多個模板實參(非類型、類型或模板)的模板形參。函數形參包是接受零個或更多個函數實參的函數形參。

lambda 初始化捕獲包是一種初始化捕獲,它為其初始化器的包展開中每個元素引入一個初始化捕獲。

(C++20 起)

結構化綁定包是結構化綁定聲明中引入一個或多個結構化綁定的標識符。

(C++26 起)

包中元素的個數等於:

  • 為形參包提供的實參的數量,若包是模板或函數形參包,
  • 其初始化器的包展開中的元素數量,若包是 lambda 初始化捕獲包,
(C++20 起)
  • 初始化器的結構化綁定大小減去結構化綁定聲明中非包元素的數量,若包是結構化綁定包。
(C++26 起)

至少有一個形參包的模板被稱作變參模板

語法

模板形參包(在別名模版類模板變量模板(C++14 起)概念(C++20 起)函數模板形參列表中出現)

類型 ... 包名 (可選) (1)
typename|class ... 包名 (可選) (2)
類型約束 ... 包名 (可選) (3) (C++20 起)
template < 形參列表 > class ... 包名 (可選) (4) (C++17 前)
template < 形參列表 > typename|class ... 包名 (可選) (4) (C++17 起)

函數形參包(聲明符的一種形式,在變參函數模板的函數形參列表中出現)

包名 ... 包形參名 (可選) (5)

有關非形參的包,參見 lambda 初始化捕獲包結構化綁定包(C++26 起)

(C++20 起)

形參包展開(在變參模板體中出現)

模式 ... (6)
1) 可以有名字的非類型模板形參包
2) 可以有名字的類型模板形參包
3) 可以有名字的受約束的類型模板形參包
(C++20 起)
4) 可以有名字的模板模板形參包
5) 可以有名字的函數形參包
6) 形參包展開:展開成零個或更多個模式 的逗號分隔列表。模式必須包含至少一個形參包。

解釋

變參類模板可以用任意數量的模板實參實例化:

template<class... Types>
struct Tuple {};

Tuple<> t0;           // Types 不包含实参
Tuple<int> t1;        // Types 包含一个实参:int
Tuple<int, float> t2; // Types 包含两个实参:int 与 float
Tuple<0> error;       // 错误:0 不是类型

變參函數模板可以用任意數量的函數實參調用(模板實參通過模板實參推導推導):

template<class... Types>
void f(Types... args);

f();       // OK:args 不包含实参
f(1);      // OK:args 包含一个实参:int
f(2, 1.0); // OK:args 包含两个实参:int 与 double

在主類模板中,模板形參包必須是模板形參列表的最後一個形參。在函數模板中,模板參數包可以在列表中更早出現,只要其後的所有形參都可以從函數實參推導或擁有默認實參即可:

template<typename U, typename... Ts>    // OK:能推导出 U
struct valid;
// template<typename... Ts, typename U> // 错误:Ts... 不在结尾
// struct Invalid;

template<typename... Ts, typename U, typename=void>
void valid(U, Ts...);    // OK:能推导出 U
// void valid(Ts..., U); // 不能使用:Ts... 在此位置是不推导语境

valid(1.0, 1, 2, 3);     // OK:推导出 U 是 double,Ts 是 {int, int, int}

如果變參模板的每個合法的特化都要求空模板形參包,那麼程序非良構,不要求診斷。

包展開

後隨省略號且其中至少有一個形參包的名字的模式會被展開 成零個或更多個逗號分隔的模式實例,其中形參包的名字按順序被替換成包中的各個元素。對齊說明符實例以空格分隔,其他實例以逗號分隔:

template<class... Us>
void f(Us... pargs) {}

template<class... Ts>
void g(Ts... args)
{
    f(&args...); // “&args...” 是包展开
                 // “&args” 是它的模式
}

g(1, 0.2, "a"); // Ts... args 会展开成 int E1, double E2, const char* E3
                // &args... 会展开成 &E1, &E2, &E3
                // Us... 会展开成 int* E1, double* E2, const char** E3

如果兩個形參包在同一模式中出現,那麼它們同時展開而且長度必須相同:

template<typename...>
struct Tuple {};

template<typename T1, typename T2>
struct Pair {};

template<class... Args1>
struct zip
{
    template<class... Args2>
    struct with
    {
        typedef Tuple<Pair<Args1, Args2>...> type;
        // Pair<Args1, Args2>... 是包展开
        // Pair<Args1, Args2> 是模式
    };
};

typedef zip<short, int>::with<unsigned short, unsigned>::type T1;
// Pair<Args1, Args2>... 会展开成
// Pair<short, unsigned short>, Pair<int, unsigned int> 
// T1 是 Tuple<Pair<short, unsigned short>, Pair<int, unsigned>>

// typedef zip<short>::with<unsigned short, unsigned>::type T2;
// 错误:包展开中的形参包包含不同长度

如果包展開內嵌於另一個包展開中,那麼它所展開的是在最內層包展開出現的形參包,並且在外圍(而非最內層)的包展開中必須提及其它形參包:

template<class... Args>
void g(Args... args)
{
    f(const_cast<const Args*>(&args)...); 
    // const_cast<const Args*>(&args) 是模式,它同时展开两个包(Args 与 args)

    f(h(args...) + args...); // 嵌套包展开:
    // 内层包展开是 “args...”,它首先展开
    // 外层包展开是 h(E1, E2, E3) + args 它其次被展开
    // (成为 h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)
}

若包中的元素個數為零(空包),則包展開的實例化不會改變其外圍構造的語法判讀,即使某些情況中完全忽略包展開則非良構或者會造成語法歧義也是如此。其實例化生成一個空列表。

template<class... Bases> 
struct X : Bases... { };

template<class... Args> 
void f(Args... args) 
{
    X<Args...> x(args...);
}

template void f<>(); // OK,X<> 没有基类
                     // x 是值初始化的 X<> 类型的对象

展開場所

展開所產生的逗號分隔(對齊說明符以空格分隔)列表按發生展開的各個場所可以是不同種類的列表:函數形參列表,成員初始化器列表,屬性列表,等等。以下列出了所有允許的語境。

函數實參列表

包展開可以在函數調用運算符的括號內出現,此時省略號左側的最大表達式或花括號包圍的初始化器列表是被展開的模式:

f(args...);              // 展开成 f(E1, E2, E3)
f(&args...);             // 展开成 f(&E1, &E2, &E3)
f(n, ++args...);         // 展开成 f(n, ++E1, ++E2, ++E3);
f(++args..., n);         // 展开成 f(++E1, ++E2, ++E3, n);

f(const_cast<const Args*>(&args)...);
// f(const_cast<const E1*>(&X1), const_cast<const E2*>(&X2), const_cast<const E3*>(&X3))

f(h(args...) + args...); // 展开成
// f(h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)

有括號初始化器

包展開可以在直接初始化器函數式轉型及其他語境(成員初始化器new 表達式等)的括號內出現,這種情況下的規則與適用於上述函數調用表達式的規則相同:

Class c1(&args...);             // 调用 Class::Class(&E1, &E2, &E3)
Class c2 = Class(n, ++args...); // 调用 Class::Class(n, ++E1, ++E2, ++E3);

::new((void *)p) U(std::forward<Args>(args)...) // std::allocator::allocate

花括號包圍的初始化器

在花括號包圍的初始化器列表中,也可以出現包展開:

template<typename... Ts>
void func(Ts... args)
{
    const int size = sizeof...(args) + 2;
    int res[size] = {1, args..., 2};
    
    // 因为初始化器列表保证顺序,所以这可以用来对包的每个元素按���序调用函数:
    int dummy[sizeof...(Ts)] = {(std::cout << args, 0)...};
}

模板實參列表

包展開可以在模板實參列表的任何位置使用,前提是模板擁有與該展開相匹配的形參:

template<class A, class B, class... C>
void func(A arg1, B arg2, C... arg3)
{
    container<A, B, C...> t1; // 展开成 container<A, B, E1, E2, E3> 
    container<C..., A, B> t2; // 展开成 container<E1, E2, E3, A, B> 
    container<A, C..., B> t3; // 展开成 container<A, E1, E2, E3, B> 
}

函數形參列表

在函數形參列表中,如果省略號在某個形參聲明中(無論它是否指名函數形參包(例如在 Args ... args 中)出現,那麼該形參聲明是模式:

template<typename... Ts>
void f(Ts...) {}

f('a', 1); // Ts... 会展开成 void f(char, int)
f(0.1);    // Ts... 会展开成 void f(double)

template<typename... Ts, int... N>
void g(Ts (&...arr)[N]) {}

int n[1];

g<const char, int>("a", n); // Ts (&...arr)[N] 会展开成 
                            // const char (&)[2], int(&)[1]

注意:在模式 Ts (&...arr)[N] 中,省略號是最內層的元素,而不是像所有其他包展開中一樣是最後的元素。

注意:不能用 Ts (&...)[N],因為 C++11 語法要求帶括號的省略號形參擁有名字:CWG 問題 1488

模板形參列表

包展開可以在模板形參列表中出現:

template<typename... T>
struct value_holder
{
    template<T... Values> // 会展开成非类型模板形参列表,
    struct apply {};      // 例如 <int, char, int(&)[5]>
};

基類說明符與成員初始化器列表

包展開可以用於指定類聲明中的基類列表。通常這也意味着它的構造函數也需要在成員初始化器列表中使用包展開,以調用這些基類的構造函數:

template<class... Mixins>
class X : public Mixins...
{
public:
    X(const Mixins&... mixins) : Mixins(mixins)... {}
};

lambda 捕獲

包展開可以在 lambda 表達式的捕獲子句中出現:

template<class... Args>
void f(Args... args)
{
    auto lm = [&, args...] { return g(args...); };
    lm();
}

sizeof... 運算符

sizeof... 也被歸類為包展開:

template<class... Types>
struct count
{
    static const std::size_t value = sizeof...(Types);
};

動態異常說明

動態異常說明中的異常列表也可以是包展開:

template<class... X>
void func(int arg) throw(X...)
{
    // ... 在不同情形下抛出不同的 X
}
(C++17 前)

對齊說明符

包展開可以在關鍵詞 alignas 所用的類型列表和表達式列表中使用。��例以空格分隔:

template<class... T>
struct Align
{
    alignas(T...) unsigned char buffer[128];
};

Align<int, short> a; // 展开后的对齐说明符是 alignas(int) alignas(short)
                     // (中间没有逗号)

屬性列表

包展開可以在屬性列表中使用,如 [[attributes...]]。例如:

template<int... args>
[[vendor::attr(args)...]] void* f();

摺疊表達式

摺疊表達式中,模式是不包含未展開的形參包的整個子表達式。

using 聲明

using 聲明中,省略號可以在聲明符列表內出現,這對於從一個形參包進行派生時有用:

template<typename... bases>
struct X : bases...
{
    using bases::g...;
};
X<B, D> x; // OK:引入 B::g 与 D::g
(C++17 起)


包索引

包索引中,包擴展是包緊隨省略號和下標。包索引表達式的模式為標識符 ,而包索引說明符的模式為 typedef 名

consteval auto first_plus_last(auto... args)
{
    return args...[0] + args...[sizeof...(args) - 1];
}

static_assert(first_plus_last(5) == 10);
static_assert(first_plus_last(5, 4) == 9);
static_assert(first_plus_last(5, 6, 2) == 7);

友元聲明

在類友元聲明中,每個類型說明符都可以後隨一個省略號:

struct C {};
struct E { struct Nested; };

template<class... Ts>
class R
{
    friend Ts...;
};

template<class... Ts, class... Us>
class R<R<Ts...>, R<Us...>>
{
    friend Ts::Nested..., Us...;
};

R<C, E> rce;           // 类 C 和 E 都是 R<C, E> 的友元
R<R<E>, R<C, int>> rr; // E::Nested 和 C 都是 R<R<E>, R<C, int>> 的友元

摺疊展開約束

摺疊展開約束中,模式是該摺疊展開約束中的約束。

摺疊展開約束不會被實例化。

(C++26 起)

註解

功能特性測試 標準 功能特性
__cpp_variadic_templates 200704L (C++11) 變參模板
__cpp_pack_indexing 202311L (C++26) 包索引

示例

下面的例子定義了類似 std::printf 的函數,並以一個值替換格式字符串中字符 % 的每次出現。

首個重載在僅傳遞格式字符串且無形參展開時調用。

第二個重載中分別包含針對實參頭的一個模板形參和一個形參包,這樣就可以在遞歸調用中只傳遞形參的尾部,直到它變為空。

Targs 是模板形參包而 Fargs 是函數形參包。

#include <iostream>

void tprintf(const char* format) // 基础函数
{
    std::cout << format;
}

template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs) // 递归变参函数
{
    for (; *format != '\0'; format++)
    {
        if (*format == '%')
        {
            std::cout << value;
            tprintf(format + 1, Fargs...); // 递归调用
            return;
        }
        std::cout << *format;
    }
}

int main()
{
    tprintf("% world% %\n", "Hello", '!', 123);
}

輸出:

Hello world! 123

缺陷報告

下列更改行為的缺陷報告追溯地應用於以前出版的 C++ 標準。

缺陷報告 應用於 出版時的行為 正確行為
CWG 1533 C++11 包展開可以在對於成員的成員初始化器中發生 已禁止
CWG 2717 C++11 對齊說明符實例以逗號分隔 以空格分隔

參閱

函數模板 定義一族函數
類模板 定義一族類
sizeof... 查詢形參包中的元素數量
C 風格的變參函數 接受可變數量的實參
預處理器宏 也可以是變參的
摺疊表達式 在二元運算符上歸約形參包
包索引 通過指定的索引訪問形參包元素。