形参包

来自cppreference.com
< cpp‎ | language

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

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

语法

模板形参包(出现于别名模版类模板变量模板函数模板形参列表中)

类型 ... Args(可选) (1)
typename|class ... Args(可选) (2)
template < 形参列表 > typename(C++17)|class ... Args(可选) (3)

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

Args ... args(可选) (4)

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

模式 ... (5)
1) 带可选名字的非类型模板形参包
2) 带可选名字的类型模板形参包
3) 带可选名字的模板模板形参包
4) 带可选名字的函数形参包
5) 形参包展开:展开成零或更多 模式 的逗号分隔列表。模式必须包含至少一个形参包。

解释

变参类模板可用任意数量的模板实参实例化:

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... Ts, typename U> struct Invalid; // 错误:Ts.. 不在结尾
 
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 起)

注解

示例

#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

上述例子定义了类似 std::printf 的函数,并以一个值替换格式字符串中字符 % 的每次出现。

首个重载在仅传递格式字符串且无形参展开时调用。

第二个重载中分别包含针对实参头的一个模板形参和一个形参包,这允许递归调用中仅传递形参的尾部,直到它变为空。

Targs 是模板形参包而 Fargs 是函数形参包

参阅

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