形参包

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

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

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

1
2
3
4
5
6
7
template<class... Types>
struct Tuple {};

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

变参函数模板可以用任意数量的函数实参调用(模板实参通过模板实参推导推导):

1
2
3
4
5
6
template<class... Types>
void f(Types... args);

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

在主类模板中,模板形参包必须是模板形参列表的最后一个形参。在函数模板中,模板参数包可以在列表中更早出现,只要其后的所有形参都可以从函数实参推导或拥有默认实参即可:

1
2
3
4
5
6
7
8
9
10
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}

如果变参模板的每个合法的特化都要求空模板形参包,那么程序非良构,不要求诊断。
包展开

后随省略号且其中至少有一个形参包的名字至少出现了一次的模式会被展开成零个或更多个逗号分隔的模式实例,其中形参包的名字按顺序被替换成包中的各个元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

如果两个形参包在同一模式中出现,那么它们同时展开而且长度必须相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
// 错误:包展开中的形参包包含不同长度

如果包展开内嵌于另一个包展开中,那么它所展开的是在最内层包展开出现的形参包,并且在外围(而非最内层)的包展开中必须提及其它形参包:

1
2
3
4
5
6
7
8
9
10
11
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)
}

展开场所

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

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

1
2
3
4
5
6
7
8
9
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 表达式等)的括号内出现,这种情况下的规则与适用于上述函数调用表达式的规则相同:

1
2
3
4
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

花括号包围的初始化器

在花括号初始化器列表(花括号包围的初始化器和其他花括号初始化器列表的列表,用于列表初始化和其他一些语境中)中,也可以出现包展开:

1
2
3
4
5
6
7
8
9
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)...};
}

模板实参列表

包展开可以在模板实参列表的任何位置使用,前提是模板拥有与该展开相匹配的形参:

1
2
3
4
5
6
7
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中)出现,那么该形参声明是模式:

1
2
3
4
5
6
7
8
9
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。
模板形参列表

包展开可以在模板形参列表中出现:

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

基类说明符与成员初始化器列表

包展开可以用于指定类声明中的基类列表。通常这也意味着它的构造函数也需要在成员初始化器列表中使用包展开,以调用这些基类的构造函数:

1
2
3
4
5
6
template<class... Mixins>
class X : public Mixins...
{
public:
X(const Mixins&... mixins) : Mixins(mixins)... {}
};

Lambda 捕获

包展开可以在 lambda 表达式的捕获子句中出现:

1
2
3
4
5
6
template<class... Args>
void f(Args... args)
{
auto lm = [&, args...] { return g(args...); };
lm();
}

sizeof… 运算符

sizeof… 也被归类为包展开:

1
2
3
4
5
template<class... Types>
struct count
{
static const std::size_t value = sizeof...(Types);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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;
}