形参包 (C++11 起)

来自cppreference.com
< cpp‎ | language

模板形参包是接受零个或更多个模板实参(非类型、类型或模板)的模板形参。函数形参包是接受零个或更多个函数实参的函数形参。

至少有一个形参包的模板被称作变参模板

语法

模板形参包(在别名模版类模板变量模板 (C++14 起)函数模板形参列表中出现)

类型 ... 包名(可选) (1)
typename|class ... 包名(可选) (2)
类型约束 ... 包名(可选) (3) (C++20 起)
template < 形参列表 > class ... 包名(可选) (4) (C++17 前)
template < 形参列表 > typename|class ... 包名(可选) (4) (C++17 起)

函数参数包(声明符的一种形式,在变参函数模板的函数形参列表中出现)

包名 ... 包形参名(可选) (5)

形参包展开(在变参模板体中出现)

模式 ... (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)
}

展开场所

展开所产生的逗号分隔列表按发生展开的各个场所可以是不同种类的列表:函数形参列表,成员初始化器列表,属性列表,等等。以下列出了所有允许的语境。

函数实参列表

包展开可以在函数调用运算符的括号内出现,此时省略号左侧的最大表达式或花括号初始化器列表是被展开的模式:

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 所用的类型列表和表达式列表中使用。

属性列表

包展开可以在属性列表中使用,如 [[attributes...]]。例如:void [[attributes...]] function()

折叠表达式

折叠表达式中,模式是不包含未展开的形参包的整个子表达式。

using 声明

using 声明中,省略号可以在声明器列表内出现,这对于从一个形参包进行派生时有用:

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

注解

示例

下面的例子定义了类似 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);
    return 0;
}

输出:

Hello world! 123

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 1533 C++11 包展开可以在对于成员的成员初始化器中发生 已禁止

参阅

函数模板
类模板
sizeof... 查询形参包中的元素数量。
C 风格的变参函数
预处理器宏 也可以是变参的
折叠表达式