Wang's blog

形参包

Published on

模板形参包是接受零个或多个模板实参(非类型、类型或模板)的模板形参。函数模板形参包是接受零个或多个函数实参的函数形参。至少有一个形参包的模板被称作变参模板。

定义

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

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();       // args不包含实参
f(1);      // args包含一个实参:int
f(2, 1.0); // args包含两个实参:int与double

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

// 正确,Ts在结尾
template <typename U, typename... Ts>
struct valid;
// 错误,Ts不在结尾
template <typename... Ts, typename U>
struct invalid;

// 正确,可以推导U
template <typename... Ts, typename U, typename = void>
void valid(U, Ts...) {}
// 错误,Ts无法推导
template <typename... Ts, typename U, typename = void>
void invalid(Ts..., U) {}

// 推导U为double,Ts为{int, int, int}
valid(1.0, 1, 2, 3);

包展开

在一个后面带有省略号的模式中,如果至少有一个形参包的名字出现了至少一次,它将被展开成零个或多个由逗号分隔的模式实例,其中形参包的名字按顺序被替换成包中的各个元素:

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... pargs展开为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 and 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
}

展开场所

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

  • 函数实参列表
  • 有括号初始化器
  • 花括号包围的初始化器
  • 模板实参列表
  • 函数形参列表
  • 模板形参列表
  • 基类说明符与成员初始化器列表
  • lambda捕获
  • sizeof…运算符
  • 动态异常说明
  • 对齐说明符
  • 属性列表
  • 折叠表达式
  • using声明