包 (C++11 起)
包是一种 C++ 实体,它定义以下各项之一:
- 形参包
- 模板形参包
- 函数形参包
(C++20 起) |
(C++26 起) |
模板形参包是接受零个或更多个模板实参(常量、类型或模板)的模板形参。函数形参包是接受零个或更多个函数实参的函数形参。
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 起) |
[编辑]解释
变参类模板可以用任意数量的模板实参实例化:
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:能推导出 Ustruct 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<unsignedshort, 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)}
若包中的元素个数为零(空包),则包展开的实例化不会改变其外围构造的语法判读,即使某些情况中完全忽略包展开则非良构或者会造成语法歧义也是如此。其实例化生成一个空列表。
template<class... Bases>struct X : Bases... {}; template<class... Args>void f(Args... args){ X<Args...> x(args...);} templatevoid f<>();// OK,X<> 没有基类// x 是值初始化的 X<> 类型的对象
[编辑]展开场所
展开所产生的逗号分隔(对齐说明符以空格分隔)列表按发生展开的各个场所可以是不同种类的列表:函数形参列表,成员初始化器列表,属性列表,等等。以下列出了所有允许的语境。
[编辑]函数实参列表
包展开可以在函数调用运算符的括号内出现,此时省略号左侧的最大表达式或花括号包围的初始化器列表是被展开的模式:
f(args...);// 展开成 f(E1, E2, 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){constint 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<constchar, 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 {staticconststd::size_t value = sizeof...(Types);};
动态异常说明动态异常说明中的异常列表也可以是包展开: template<class... X>void func(int arg)throw(X...){// ... 在不同情形下抛出不同的 X} | (C++17 前) |
[编辑]对齐说明符
包展开可以在关键词 alignas
所用的类型列表和表达式列表中使用。实例以空格分隔:
template<class... T>struct Align { alignas(T...)unsignedchar buffer[128];}; Align<int, short> a;// 展开后的对齐说明符是 alignas(int) alignas(short)// (中间没有逗号)
[编辑]属性列表
包展开可以在属性列表中使用,如 [[attributes...]]。例如:
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 与 D::g | (C++17 起) |
包索引在包索引中,包扩展是包紧随省略号和下标。包索引表达式的模式为标识符 ,而包索引说明符的模式为 typedef 名。 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;// 类 C 和 E 都是 R<C, E> 的友元 R<R<E>, R<C, int>> rr;// E::Nested 和 C 都是 R<R<E>, R<C, int>> 的友元 折叠展开约束在折叠展开约束中,模式是该折叠展开约束中的约束。 折叠展开约束不会被实例化。 | (C++26 起) |
[编辑]注解
本节未完成 原因:关于部分特化和其他访问单独元素方式的一些话?提及递归 vs 对数 vs 短路,例如折叠表达式 |
功能特性测试宏 | 值 | 标准 | 功能特性 |
---|---|---|---|
__cpp_variadic_templates | 200704L | (C++11) | 变参模板 |
__cpp_pack_indexing | 202311L | (C++26) | 包索引 |
[编辑]示例
下面的例子定义了类似 std::printf 的函数,并以一个值替换格式字符串中字符 %
的每次出现。
首个重载在仅传递格式字符串且无形参展开时调用。
第二个重载中分别包含针对实参头的一个模板形参和一个形参包,这样就可以在递归调用中只传递形参的尾部,直到它变为空。
Targs
是模板形参包而 Fargs
是函数形参包。
#include <iostream> void tprintf(constchar* format)// 基础函数{std::cout<< format;} template<typename T, typename... Targs>void tprintf(constchar* 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);}
输出:
Hello world! 123
[编辑]缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 1533 | C++11 | 包展开可以在对于成员的成员初始化器中发生 | 已禁止 |
CWG 2717 | C++11 | 对齐说明符实例以逗号分隔 | 以空格分隔 |
[编辑]参阅
函数模板 | 定义一族函数 |
类模板 | 定义一族类 |
sizeof... | 查询形参包中的元素数量 |
C 风格的变参函数 | 接受可变数量的实参 |
预处理器宏 | 也可以是变参的 |
折叠表达式 | 在二元运算符上归约形参包 |
包索引 | 通过指定的索引访问形参包元素。 |