Pack (自 C++11 起)
包是 C++ 实体,它定义了以下内容之一
- 形参包
- 模板形参包
- 函数形参包
(自 C++20 起) |
(自 C++26 起) |
模板形参包是接受零个或多个模板实参(非类型、类型或模板)的模板形参。函数形参包是接受零个或多个函数实参的函数形参。
lambda 初始化捕获包是一个 lambda 捕获,它为其初始化程序的包展开中的每个元素引入一个初始化捕获。 |
(自 C++20 起) |
结构化绑定包是结构化绑定声明中的标识符,它引入零个或多个结构化绑定。 |
(自 C++26 起) |
包的元素数量等于
- 为形参包提供的实参数量(如果包是模板或函数形参包),
|
(自 C++20 起) |
|
(自 C++26 起) |
具有至少一个形参包的模板称为可变参数模板。
目录 |
[编辑] 语法
模板形参包(出现在别名模板、类模板、变量模板(自 C++14 起)、概念(自 C++20 起)和函数模板形参列表)
type ... pack-name (可选) |
(1) | ||||||||
typename | class ... pack-name (可选) |
(2) | ||||||||
type-constraint ... pack-name (可选) |
(3) | (自 C++20 起) | |||||||
template < parameter-list > class ... pack-name (可选) |
(4) | (直到 C++17) | |||||||
template < parameter-list > typename | class ... pack-name (可选) |
(4) | (自 C++17 起) | |||||||
函数形参包(声明符的一种形式,出现在可变参数函数模板的函数形参列表中)
pack-name ... pack-param-name (可选) |
(5) | ||||||||
对于非形参包的语法,参见lambda 初始化捕获包 和结构化绑定包(自 C++26 起)。 |
(自 C++20 起) |
包展开(出现在模板主体中)
pattern ... |
(6) | ||||||||
3) 具有可选名称的受约束的类型模板形参包 |
(自 C++20 起) |
pattern
的列表。模式必须至少包含一个包。[编辑] 解释
可变参数类模板可以使用任意数量的模板实参实例化
template<class... Types> struct Tuple {}; Tuple<> t0; // Types contains no arguments Tuple<int> t1; // Types contains one argument: int Tuple<int, float> t2; // Types contains two arguments: int and float Tuple<0> t3; // error: 0 is not a type
可变参数函数模板可以使用任意数量的函数实参调用(模板实参通过模板实参推导推导)
template<class... Types> void f(Types... args); f(); // OK: args contains no arguments f(1); // OK: args contains one argument: int f(2, 1.0); // OK: args contains two arguments: int and double
在主类模板中,模板形参包必须是模板形参列表中的最后一个形参。在函数模板中,模板形参包可以出现在列表的较早位置,前提是所有后续形参都可以从函数实参中推导出来,或者具有默认实参
template<typename U, typename... Ts> // OK: can deduce U struct valid; // template<typename... Ts, typename U> // Error: Ts... not at the end // struct Invalid; template<typename... Ts, typename U, typename=void> void valid(U, Ts...); // OK: can deduce U // void valid(Ts..., U); // Can't be used: Ts... is a non-deduced context in this position valid(1.0, 1, 2, 3); // OK: deduces U as double, Ts as {int, int, int}
如果可变参数模板的每个有效特化都需要一个空模板形参包,则程序是非良构的,不需要诊断。
[编辑] 包展开
后跟省略号的模式,其中至少一个包的名称至少出现一次,将被展开为零个或多个模式的实例化,其中包的名称按顺序替换为包中的每个元素。对齐说明符的实例化用空格分隔,其他实例化用逗号分隔。
template<class... Us> void f(Us... pargs) {} template<class... Ts> void g(Ts... args) { f(&args...); // “&args...” is a pack expansion // “&args” is its pattern } g(1, 0.2, "a"); // Ts... args expand to int E1, double E2, const char* E3 // &args... expands to &E1, &E2, &E3 // Us... pargs expand to 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>... is the pack expansion // Pair<Args1, Args2> is the pattern }; }; typedef zip<short, int>::with<unsigned short, unsigned>::type T1; // Pair<Args1, Args2>... expands to // Pair<short, unsigned short>, Pair<int, unsigned int> // T1 is Tuple<Pair<short, unsigned short>, Pair<int, unsigned>> // typedef zip<short>::with<unsigned short, unsigned>::type T2; // error: pack expansion contains packs of different lengths
如果包展开嵌套在另一个包展开中,则出现在最内层包展开中的包由它展开,并且在外层包展开中必须提到另一个包,但不能在最内层包展开中提到
template<class... Args> void g(Args... args) { f(const_cast<const Args*>(&args)...); // const_cast<const Args*>(&args) is the pattern, it expands two packs // (Args and args) simultaneously f(h(args...) + args...); // Nested pack expansion: // inner pack expansion is "args...", it is expanded first // outer pack expansion is h(E1, E2, E3) + args..., it is expanded // second (as 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<> has no base classes // x is a variable of type X<> that is value-initialized
[编辑] 展开位置
根据展开发生的位置,生成的逗号分隔(或空格分隔,对于对齐说明符)列表是不同类型的列表:函数形参列表、成员初始化器列表、属性列表等。以下是所有允许的上下文列表
[编辑] 函数实参列表
包展开可能出现在函数调用运算符的括号内,在这种情况下,省略号左侧的最大表达式或花括号包围的初始化器列表是展开的模式
f(args...); // expands to f(E1, E2, E3) f(&args...); // expands to f(&E1, &E2, &E3) f(n, ++args...); // expands to f(n, ++E1, ++E2, ++E3); f(++args..., n); // expands to 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...); // expands to // f(h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)
[编辑] 带括号的初始化器
包展开可能出现在直接初始化器、函数式转型以及其他上下文(成员初始化器、new-表达式等)的括号内,在这种情况下,规则与上面函数调用表达式的规则相同
Class c1(&args...); // calls Class::Class(&E1, &E2, &E3) Class c2 = Class(n, ++args...); // calls 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}; // since initializer lists guarantee sequencing, this can be used to // call a function on each element of a pack, in order: 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; // expands to container<A, B, E1, E2, E3> container<C..., A, B> t2; // expands to container<E1, E2, E3, A, B> container<A, C..., B> t3; // expands to container<A, E1, E2, E3, B> }
[编辑] 函数形参列表
在函数形参列表中,如果省略号出现在形参声明中(无论它是否命名函数形参包(如,Args...
args)),则形参声明是模式
template<typename... Ts> void f(Ts...) {} f('a', 1); // Ts... expands to void f(char, int) f(0.1); // Ts... expands to 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] expands to // const char (&)[2], int(&)[1]
注意:在模式 Ts (&...arr)[N]
中,省略号是最内层元素,而不是像所有其他包展开中那样的最后一个元素。
注意:Ts (&...)[N]
是不允许的,因为 C++11 语法要求带括号的省略号具有名称:CWG 问题 1488。
[编辑] 模板形参列表
包展开可能出现在模板形参列表中
template<typename... T> struct value_holder { template<T... Values> // expands to a non-type template parameter struct apply {}; // list, such as <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...) { // ... throw different Xs in different situations } |
(直到 C++17) |
[编辑] 对齐说明符
关键字 alignas
使用的类型列表和表达式列表都允许包展开。实例化用空格分隔
template<class... T> struct Align { alignas(T...) unsigned char buffer[128]; }; Align<int, short> a; // the alignment specifiers after expansion are // alignas(int) alignas(short) // (no comma in between)
[编辑] 属性列表
如果属性的规范允许,则属性列表中允许包展开。例如
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 and D::g introduced |
(自 C++17 起) |
包索引在包索引中,包展开包含一个未展开的包,后跟省略号和下标。包索引表达式的模式是 identifier,而包索引说明符的模式是 typedef-name。 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; // classes C and E are friends of R<C, E> R<R<E>, R<C, int>> rr; // E::Nested and C are friends of R<R<E>, R<C, int>> 折叠展开的约束在折叠展开的约束中,模式是该折叠展开约束的约束。 折叠展开的约束不会被实例化。 |
(自 C++26 起) |
[编辑] 注释
本节尚不完整 原因:关于偏特化和其他访问单个元素的方式的几句话?提及递归与对数与折叠表达式等快捷方式的比较 |
特性测试 宏 | 值 | Std | 特性 |
---|---|---|---|
__cpp_variadic_templates |
200704L |
(C++11) | 可变参数模板 |
__cpp_pack_indexing |
202311L |
(C++26) | 包索引 |
[编辑] 示例
以下示例定义了一个类似于 std::printf 的函数,该函数将格式字符串中每次出现的字符 %
替换为一个值。
当仅传递格式字符串且没有形参展开时,将调用第一个重载。
第二个重载包含一个单独的模板形参,用于实参的头部和一个形参包,这允许递归调用仅传递实参的尾部,直到它为空。
Targs
是模板形参包,Fargs
是函数形参包。
#include <iostream> void tprintf(const char* format) // base function { std::cout << format; } template<typename T, typename... Targs> void tprintf(const char* format, T value, Targs... Fargs) // recursive variadic function { for (; *format != '\0'; format++) { if (*format == '%') { std::cout << value; tprintf(format + 1, Fargs...); // recursive call return; } std::cout << *format; } } int main() { tprintf("% world% %\n", "Hello", '!', 123); }
输出
Hello world! 123
[编辑] 缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的 C++ 标准。
DR | 应用于 | 已发布行为 | 正确行为 |
---|---|---|---|
CWG 1533 | C++11 | 包展开可能发生在成员的成员初始化器中 | 不允许 |
CWG 2717 | C++11 | 对齐说明符的实例化用逗号分隔 | 它们用空格分隔 |
[编辑] 参见
函数模板 | 定义函数族 |
类模板 | 定义类族 |
sizeof...
|
查询包中元素的数量 |
C 风格可变参数函数 | 接受可变数量的实参 |
预处理器宏 | 也可以是可变参数的 |
折叠表达式 | 通过二元运算符缩减包 |
包索引 | 访问包中指定索引处的元素 |