包 (自 C++11 起)
包是定义以下内容之一的 C++ 实体
- 参数包
- 模板参数包
- 函数参数包
(C++20 起) |
(C++26 起) |
模板参数包是接受零个或多个模板实参(非类型、类型或模板)的模板参数。函数参数包是接受零个或多个函数实参的函数参数。
lambda 初始化捕获包是一种 lambda 捕获,它为其初始化器中包展开的每个元素引入一个初始化捕获。 |
(C++20 起) |
结构化绑定包是结构化绑定声明中的一个标识符,它引入零个或多个结构化绑定。 |
(C++26 起) |
包的元素数量等于
- 如果包是模板或函数参数包,则为参数包提供的实参数量,
|
(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) | ||||||||
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 issue 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 起) |
包索引在包索引中,包展开包含一个未展开的包,后跟省略号和下标。包索引表达式的模式是标识符,而包索引说明符的模式是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); Friend 声明在类friend 声明中,每个类型说明符后都可以跟一个省略号 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 起) |
[编辑] 注意
本节不完整 原因:关于部分特化和其他访问单个元素的方法的几句话?提及递归与对数与折叠表达式等快捷方式 |
特性测试宏 | 值 | 标准 | 特性 |
---|---|---|---|
__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++ 标准。
缺陷报告 | 应用于 | 发布时的行为 | 正确的行为 |
---|---|---|---|
CWG 1533 | C++11 | 包展开可以出现在成员的成员初始化器中 | 不允许 |
CWG 2717 | C++11 | 对齐说明符的实例化以逗号分隔 | 它们以空格分隔 |
[编辑] 另请参阅
函数模板 | 定义函数族 |
类模板 | 定义类族 |
sizeof...
|
查询包中的元素数量 |
C 风格可变参数函数 | 接受可变数量的参数 |
预处理器宏 | 也可以是可变参数的 |
折叠表达式 | 通过二元运算符约化一个包 |
包索引 | 访问包中指定索引处的元素 |