参数包

来自cppreference.com
< cpp‎ | language

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

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

目录

[编辑] 语法

模板参数包(出现于类模板函数模板形参列表)

type ... Args(可选) (1) (C++11 起)
typename|class ... Args(可选) (2) (C++11 起)
template < parameter-list > typename(C++17)|class ... Args(可选) (3) (C++11 起)

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

Args ... args(可选) (4) (C++11 起)

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

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

[编辑] 解释

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

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> // 展开成无类型模板参数列表,例如 <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 风格变参数函数
预处理器宏 亦可为变参数