回到顶部 暗色模式

C++ Templates(7):深入模版基础

1. 参数化的声明

        C++现在支持四种基础模版类型:类模版、函数模版、变量模版和别名模版,每个模版类型都能在命名空间作用域和类作用域中使用。在类作用域内,它们可以作为类内类模版、成员函数模版、静态数据成员模版和成员别名模版。这种模版的声明类似于普通类、函数、变量和类型别名,除了使用参数化子句开头外。C++17还引入了另一种使用参数化子句的结构——推导指引。
        首先,让我们看一下四种模版的例子。首先是命名空间作用域:

template<typename T>  // 命名空间作用域类模版
class Data {
public:
  static constexpr bool copyable = true;
  // ...
};

template<typename T>  // 命名空间作用域函数模版
void log(T x) {
  // ...
}

template<typename T>  // 命名空间作用域变量模版
T zero = 0;

template<typename T>  // 命名空间作用域变量模版
bool dataCopyable = Data<T>::copyable;

template<typename T>  // 命名空间作用域别名模版
using DataList = Data<T*>;

        接下来是类作用域:

class Collection {
public:
  template<typename T>  // 类内成员类模版
  class Node {
    // ...
  };

  template<typename T>  // 类内成员函数模版
  T* alloc() {  // 提供了定义,是隐式内联的
    // ...
  }

  template<typename T>  // 成员变量模版
  static T zero = 0;

  template<typename T>  // 成员别名模版
  using NodePtr = Node<T>*;
};

        在C++17,变量 ( 包括静态数据成员 ) 和变量模版可以“内联”,这意味着它们可以在翻译单元间重复定义。与成员函数不同,一个定义在封闭类中的静态数据成员不是内联的,必须在所有地方都指定 $inline$ 才行。
        最后一段代码演示除别名模版外,其他模版都可以在类外定义:

template<typename T>
class List {
public:
  List() = default;

  template<typename U>
  class Handle;

  template<typename U>
  List(List<U> const&);

  template<typename U>
  static U zero;
};

template<typename T>
template<typename U>
class List<T>::Handle {
  // ...
};

template<typename T>
template<typename T2>
List<T>::List(List<T2> const& b) {
  // ...
}

template<typename T>
template<typename U>
U List<T>::zero = 0;

        定义在类外的成员模版可能需要多个 $template$<$\dots$> 语句,从最外层到最内层的方式排列。注意一个构造函数模版会使类不生成隐式的默认构造函数,所以我们需要添加一个 $default$ 声明。
        联合也可以使用模版:

template<typename T>
union AllocChunk {
  T object;
  unsigned char bytes[sizeof(T)];
};

        函数模版也可以有默认调用参数:

template<typename T>
void report_top(Stack<T> const&, int number = 10);

template<typename T>
void fill(Array<T>&, T const& = T{});

        当 $fill$ 被调用时,如果提供了第二个参数,那么默认参数就不会实例化。这样就可以确保当 $T$ 没有默认构造函数时不会报错了。
        类内除了可以声明四种基本模版类型外,你也可以让普通类成员通过成为类模版成员的方式实现参数化。它们通常被错误地称为成员模版。尽管可以被参数化,但是这种定义跟第一类模版是不同的,它们的参数是完全由它们所属的模版决定的。例如:

template<int I>
class CupBoard {
  class Shelf;
  void open();
  enum Wood : unsigned char;
  static double totalWeight;
};

        相应的定义只会在父类模版中使用参数化子句,不会在成员中使用,因为它们不是模版:

template<int I>
class CupBoard<I>::Shelf {
  // ...
};

template<int I>
void CupBoard<I>::open() {
  // ...
}

template<int I>
enum CupBoard<I>::Wood {
  Maple, Cherry, Oak
};

template<int I>
double CupBoard<I>::totalWeight = 0.0;

        从C++17开始,静态 $totalWeight$ 成员可以在类内模版中使用 $inline$ :

template<int I>
class CupBoard {
  inline static double totalWeight = 0.0;
  // ...
};

        尽管这种参数化定义通常被称为模版,但是这个名词不太适合它们。一个偶尔被建议用于称呼这些实体的名词是 $temploid$ 。从C++17开始,C++标准没有定义模版化实体 ( $templated$ $entity$ ) 的概念,它通常包含模版和 $temploid$ ,以及任何在模版实体中定义和创建的实体。

1.1 虚成员函数

        成员函数模版不能作为虚函数。因为通常虚函数调用机制是通过一个固定大小的表,表项指向每一个虚函数。然而,只有当整个程序都被翻译完成后,成员函数模版的数量才能确定。因此,虚成员函数模版的支持,需要C++编译器和链接器提供一种新机制。

1.2 模版的链接性

        每个模版必须有个名字,并且在当前作用域内,这个名字必须是独一无二的,除非是可重载的函数模版。与类类型不同,类模版不允许在不同类型实体之间使用相同的名字:

int C;
class C;

int X;
template<typename T>
class X;  // 错误

        模版名具有链接性,但他们不能有C链接性。非标准的链接性可能具有依赖于实现的含义 ( 然而,我们并不知道支持模版的非标准名称链接性的实现 ):

extern "C++" template<typename T>
void normal();  // 默认,可以省略链接性的指定

extern "C" template<typename T>
void invalid();  // 错误

extern "Java" template<typename T>
void javaLink();  // 非标准,但是可能有些编译器在将来会支持

        模版通常具有外部链接性。除了命名空间中指定为 $static$ 的函数模版、直接或间接作为匿名函数空间成员的模版 ( 具有内部链接性 ) 和匿名类成员模版 ( 没有链接性 )。例如:

template<typename T>  // 作为另一个文件中名称相同的实体的声明
void external();

template<typename T>  // 与另一个文件中名称相同模版没有关联
static void internal();

template<typename T>  // 再次声明
static void internal();

namespace {
  template<typename>  // 与另一个文件中名称相同的模版没有关联
  void otherInternal();  // 即使那个模版也在匿名命名空间内
}

namespace {
  template<typename>  // 再次声明
  void otherInternal();
}

struct {
  template<typename T>  // 无链接性,不能被再次声明
  void f(T) {}
} x;

        注意由于最后的成员模版没有链接性,所以它必须与匿名类一起定义,因为没有办法在类外提供定义。
        通常模版不能在函数作用域或者局部类作用域中声明,但是泛型lambda,它关联闭包类型且包含成员函数模版,可以在局部作用域中使用。
        模版实例的链接性与模版的链接性相同。例如,函数 $internal$<$void$> 是上面声明的模版的实例化,它与模版一样具有内部链接性。对于变量模版,这会产生有趣的结果:

template<typename T> T zero = T{};

        所有 $zero$ 的实例化都会具有外部链接性,即使是 $zero$<$int$ $const$> 。这可能有点反直觉,因为:

int const zero_int = int{};

        是具有内部链接性的,因为它声明为 $const$ 。类似的:

template<typename T>
int const max_volume = 11;

        所有这个模版的实例化都具有外部链接性,尽管这些实现都具有 $const$ 限定符。

1.3 主模版

        一般的模版声明称为主模版 ( $primary$ $templates$ ),这种模版声明没有在模版名后添加尖括号。

template<typename T> class Box;  // 主模版
template<typename T> class Box<T>;  // 错误,没有特例化

template<typename T> void translate(T);  // 主模版
template<typename T> void translate<T>(T);  // 错误,不允许这样声明函数

template<typename T> constexpr T zero = T{};  // 主模版
template<typename T> constexpr T zero<T> = T{};  // 错误,没有特例化

        非主模版在声明类部分特例化或这变量模版时出现。

2. 模版参数

        模版参数有三种基本类型:

  1. 类型参数 ( 最常见的 );
  2. 非类型参数;
  3. 模版模版参数。

        任意一种类型都可以作为模版参数集合元素。模版参数在模版参数化子句中声明,如果模版中需要参数名,那么在定义时需要给出参数名。

2.1 类型参数

        类型参数可以使用 $typename$ 关键字或者 $class$ 关键字,它们是等价的。关键字之后应该是一个简单的标识符,在下一个类型参数的开始之前需要使用逗号隔开。类型参数的声明使用尖括号代表开始和结束,中间可以使用 $=$ 声明默认模版参数。
        在模版声明中,类型参数类似于一个类型别名。例如,当 $T$ 是一个模版参数时,你无法使用它的全名:

template<typename Allocator>
class List {
  class Allocator* allocptr;  // 错误
  friend class Allocator;  // 错误
  // ...
};

2.2 非类参数

        非类模版参数代表可以在编译期或者链接期确定的常量。这样的参数必须是:

        其他类型暂不支持。出乎意料的是,非类模版参数的声明有时候也可以使用 $typename$ :

template<typename T,
         typename T::Allocator* Allocator>
class List;

        或者使用 $class$ :

template<class X*>
class Y;

        这种情况很容易区分,因为它们之后都是标识符。
        也可以指定函数和数组类型,但是它们会被隐式转为指针类型:

template<int buf[5]> class Lexer;  // 实际上是int*
template<int* buf> class Lexer;  // 再次声明

template<int fun()> struct FuncWrap;  // 实际上是函数指针
template<int (*)()> struct FuncWrap;  // 再次声明

        非类模版参数的声明类似于变量,但是不能具有非类标识符,像 $static$ 、$mutable$ 等。它们可以有 $const$ 和 $volatile$ 标识符,但是如果这种标识符在最外层的模版参数类型中使用就会被忽略:

template<int const length> class Buffer;  // const会被忽略
template<int length> class Buffer;  // 与上一种一样

        最后,非引用的非类模版参数的地址是无法获取的,所以无法被赋值。一个左值引用非类模版参数则可以这样使用:

template<int& Counter>
struct LocalIncrement {
  LocalIncrement() { Counter = Counter + 1; }
  ~LocalIncrement() { Counter = Counter - 1; }
};

        右值就不可以这样做了。

2.3 模版模版参数

        模版模版参数是类模版或者别名模版的占位符。它们的声明方式类似于类模版,但是不能使用 $struct$ 和 $union$ :

template<template<typename X> class C>
void f(C<int>* p);

template<template<typename X> struct C>  // 错误
void f(C<int>* p);

template<template<typename X> union C>  // 错误
void f(C<int>* p);

        C++17允许使用 $typename$ 而不是 $class$ ,这个改变可能是因为模版模版参数不仅可以被类模版代替,也可以被别名模版代替。所以,C++17中可以这样写:

template<template<typename X> typename C>
void f(C<int>* p);

        在它们的声明中,模版模版参数就类似于其他类或者别名模版。
        模版模版参数也可以有默认模版参数,当对应的参数没有指定时就会使用它们:

template<template<typename T,
                  typename A = MyAllocator> class Container>
class Adaptation {
  Container<int> storage;  // 隐式等价于Container<int, MyAllocator>
  // ...
};

        $T$ 和 $A$ 是模版模版参数 $Container$ 的模版参数名,这个模版模版参数的名字只在其他模版的声明中使用。下面这个故意的例子展示了这个概念:

template<template<typename T, T*> class Buf>
class Lexer {
  static T* storage;  // 错误
  // ...
};

        通常,模版模版参数的模版参数名不需要在其他模版参数的声明中使用,因此也可以声明为匿名。例如:

template<template<typename,
                  typename = MyAllocator> class Container>
class Adaptation {
  Container<int> storage;  // 隐式等价于Container<int, MyAllocator>
  // ...
};

2.4 模版参数集合

        从C++11开始,任何类型的模版参数都可以作为模版参数集合使用,只需要在模版参数名前添加 $/dots$ 。匿名参数也可以使用模版参数集合。

template<typename... Types>
class Tuple;

        一个模版参数集合的行为类似于它的底层模版参数,但是有一个重要不同之处:一个普通模版参数精准匹配一个模版传入参数,而一个模版参数集合可以匹配任意数量的模版传入参数。这意味着上面声明的 $Tuple$ 可以接收任意数量的类型:

using IntTuple = Tuple<int>;
using IntCharTuple = Tuple<int, char>;
using IntTriple = Tuple<int, int, int>;
using EmptyTuple = Tuple<>;

        类似的,模版参数集合也可以接收任意数量非类模版参数和模版模版参数。相对的:

template<typename T, unsigned... Dimensions>
class MultiArray;

using TransformMatrix = MultiArray<double, 3, 3>

template<typename T, template<typename, typename>... Containers>
void testContainers();

        C++17还引入了非类模版参数的推导。
        主类模版、变量模版和别名模版可以含有最多一个模版参数集合,并且模版参数集合只能在模版参数的最后。函数模版有着相对宽松的限制:允许多个模版参数集合,只要每个模版参数集合之后模版参数都有一个默认值,或者可以被推导。

template<typename... Types, typename Last>
class LastType;  // 错误,模版参数集合不是最后一个模版参数

template<typename... TestTypes, typename T>
void runTests(T value);  // 模版参数集合之后的模版参数可以被推导

template<unsigned...> struct Tensor;
template<unsigned... Dims1, unsigned... Dims2>
auto compose(Tensor<Dims1...>, Tensor<Dims2...>);

        类和变量模版的部分实例化声明也可以有多个参数集合,因为部分实例化的匹配机制类似于函数模版的匹配机制。

template<typename...> Typelist;
template<typename X, typename Y> struct Zip;
template<typename... Xs, typename... Ys>
struct Zip<Typelist<Xs...>, Typelist<Ys...>>;

        一个类型参数集合不能在它自己的参数子句中展开,比如:

template<typename... Ts, Ts... vals>
struct StaticValues{};

        然而,类内模版可以使用相似的语句:

template<typename... Ts> strcut ArgList {
  template<Ts... vals> struct Vals{};
};

ArgList<int, char, char>::Vals<3, 'x', 'y'> tada;

        包含模版参数集合的模版称为变长模版,因为它可以接收任意数量的模版参数。

2.5 默认模版参数

        任意种类的非模版参数集合的模版参数都可以指定默认参数,不过只能指定对应参数类型的值。默认参数不能依赖于它自己的参数,因为参数名并不在作用域内。但是可以依赖于之前的参数:

template<typename T, typename Allocator = allocator<T>>
class List;

        类模版、变量模版或者别名模版的模版参数都可以指定一个默认参数,如果指定了默认参数,那么后续参数也要指定默认参数 ( 类似的限制也适用于默认函数调用参数 )。后续的默认值通常在同个模版声明中提供,但是它们也可以在该模版先前声明中声明。例如:

template<typename T1, typename T2, typename T3,
         typename T4 = char, typename T5 = char>
class Quintuple;

template<typename T1, typename T2, typename T3 = char,
         typename T4, typename T5>
class Quintuple;  // 之前已经声明了T4和T5的默认值

template<typename T1 = char, typename T2, typename T3,
         typename T4, typename T5>
class Quintuple;  // 错误,T2没有默认值

        函数模版的默认模版参数不需要后续的默认参数:

template<typename R = void, typename T>
R* addressof(T& value);

        默认模版参数不能重复声明:

template<typename T = void>
class Value;

template<typename T =void>
class Value;  // 错误

        有几种情况不能使用默认模版参数:

template<typename T>
class C;

template<typename T = int>
class C<T*>;  // 错误
template<typename... Ts = int> struct X;  // 错误
template<typename T>
struct X {
  T f();
};

template<typename T = int>
T X<T>::f() {  // 错误
  // ...
}
struct S {
  template<typename = void>
  friend struct F;  // 错误
};
struct S {
  template<typename = void> friend void f();  // 错误
  template<typename = void> friend void g() {}
};

template<typename> void g();  // 错误

3. 模版传入参数

        当模版实例化时,模版参数会被模版传入参数代替。入参可以通过几种不同的机制指定:

3.1 函数模版入参

        函数模版的模版入参可以显式指定,从模版使用方式或者默认模版入参中推导,例如:

template<typename T>
T max(T a, T b) {
  return b < a ? a : b;
}

int main() {
  ::max<double>(1.0, -3.0);  // 显式指定
  ::max(1.0, -3.0);  // 隐式推导
  ::max<int>(1.0, 3.0);  // 显式指定为int
}

        有些模版入参永远也不会被推导,因为它们相应的模版参数不在函数参数类型中出现,一些其他原因也会导致这种情况出现。相应的参数通常位域模版参数列表中的首位,从而我们可以只显式指定该入参,让其他入参隐式推导。例如:

template<typename DstT, typename SrcT>
DstT implicit_cast(SrcT const& x) {
  return x;
}

int main() {
  double value = implicit_cast<double>(-1);
}

        如果我们颠倒了上面模版参数的声明顺序,那么我们就需要显式指定所有模版入参。而且,这种参数不能位于模版参数集合后面,也不能部分特例化,因为这样就不能进行显式指定或者推导了:

template<typename... Ts, int N>
void f(double (&)[N + 1], Ts... ps);  // N无法指定或推导

        因为函数模版可以重载,显式提供一个函数模版的所有入参可能还不足以确定一个函数。下面的例子就展示了这种问题:

template<typename Func, typename T>
void apply(Func funcPtr, T x) {
  funcPtr(x);
}

template<typename T> void single(T);

template<typename T> void multi(T);
template<typename T> void multi(T*);

int main() {
  apply(&single<int>, 3);
  apply(&multi<int>, 7);  // 错误
}

        在这个例子中,第一个调用成功,因为没有歧义。第二个调用中,$multi$<$int$> 可以是两个版本中的任意一个,因此无法推导。
        更进一步,使用模版入参代替函数模版可能会产生一个无效的C++类型或者表达式。例如:

template<typename T> RT1 test(typename T::X const*);
template<typename T> RT2 test(...);

        这个例子中 $test$<$int$> 在第一个模版中是无意义的,因为它没有成员 $X$ 。但是第二个模版不存在这样的问题。因此,表达式 &$test$<$int$> 就可以确定一个函数。这个原则称为SFINAE ( $substitution$ $failure$ $is$ $not$ $an$ $error$ )。

3.2 类型入参

        模版类型参数指定的值就是模版类型入参。任意类型都可以作为模版类型入参,但是在使用入参替代之后的结构应该是一个有效的结构:

template<typename T>
void clear(T p) {
  *p = 0;  // 应该可以使用解引用运算符
}

int main() {
  int a;
  clear(a);  // 错误,不能使用解引用运算符
}

3.3 非类入参

        非类模版参数指定的值是非类模版入参。这种值必须是下面其中一种:

        对于非类整型参数,最常见的是可以隐式转为参数类型的入参。C++11引入了 $constexpr$ 函数,意味着入参在被转换前也可以是类类型。C++17之前,当入参是引用指针时,用户定义的转换 ( 单参数构造函数和转换运算符 ) 和子类到基类的转换是不会进行的,即使它们可以隐式进行。给参数加上 $const$ 或者 $volatile$ 的隐式转换是可以的。

template<typename T, T nontypeParam>
class C;

C<int, 33>* c1;  // 整型

int a;
C<int*, &a>* c2;  // 外部变量地址

void f();
void f(int);
C<void (*)(int), f>* c3;  // 函数名,隐式取地址

template<typename T> void templ_func();
C<void(), &templ_func<double>>* c4;  // 实例化的函数模版

struct X {
  static bool b;
  int n;
  constexpr operator int() const { return 42; }
};

C<bool&, X::b>* c5;  // 静态类成员
C<int X::*, &X::n>* c6;  // 成员指针常量
C<long, X{}>* c7;  // X通过constexpr隐式转为int,再转为long

        模版入参的一般限制是编译器或者链接器必须在程序编译时可以表示出它们的值,程序运行时才能获得的值是不行的。当然,有些值也不行:

        字符串常量的一个问题是两个值相同的字符串可以存储在不同地址中。一个可选 ( 但是麻烦 ) 的方法是使用一个额外的变量来指代常量字符串:

template<char const* str>
class Message {
  // ...
};

extern char const hello[] = "Hello World!";
char const hello11[] = "Hello World!";

void foo() {
  static char const hello17[] = "Hello World!";

  Message<hello> msg03;
  Message<hello11> msg11;  // C++11开始
  Message<hello17> msg17;  // C++17开始
}

        在C++所有版本中,声明为引用或者指针的非类模版参数可以接收一个具有外部链接的常量表达式;C++11开始,常量表达式也可以具有内部链接;C++17开始,常量表达式可以具有任何链接。

template<typename T, T nontypeParam>
class C;

struct Base {
  int i;
} base;

struct Derived : public Base {
} derived;

C<Base*, &derived>* err1;  // 错误,不能进行子类到基类的转换
C<int&, base.i>* err2;  // 错误,变量字段不能作为变量

int a[10];
C<int*, &a[0]>* err3;  // 错误,不能使用数组元素地址

3.4 模版模版入参

        一个模版模版入参必须是一个与它将代替的模版模版参数类型精确匹配的类模版或者别名模版。在C++17之前,模版模版入参的默认模版参数会被忽略 ( 声明模版模版参数时指定的默认值不会 )。C++17放宽了这种限制,允许模版模版入参对应的模版模版参数是一个特例化版本。下面的代码在C++17之前是错误的:

#include <list>

template<typename T1, typename T2,
         template<typename> class Cont>
class Rel {
  // ...
};

Rel<int, double, std::list> rel;  // C++17之前是错误的

        因为 $std::list$ 模版参数在标准库中不止一个,所以会产生错误。尽管 $std::list$ 的第二个参数具有默认值,但是在C++17之前,它们还是会被忽略。
        变长模版模版参数是一个例外,它不会因为上面的原因而产生错误,这个问题有一个解决方案:它们可以对模版模版参数进行一般匹配而非精确匹配。一个模版模版参数集合可以匹配任意数量的相同类型的模版参数:

#include <list>

template<typename T1, typename T2,
         template<typename...> class Cont>
class Rel {
  // ...
};

Rel<int, double, std::list> rel;

        模版参数集合只能匹配相同种类的模版入参。例如,下面的模版参数可以被任意只具有模版类型参数的类模版或者别名模版实例化,因为模版类型参数集合可以匹配任意数量的参数:

#include <list>
#include <map>
#include <array>

template<typename<typename...> class TT>
class AlmostAnyTmpl {};

AlmostAnyTmpl<std::vector> withVector;
AlmostAnyTmpl<std::map> withMap;
AlmostAnyTmpl<std::array> withArray;

        在C++17之前,只有 $class$ 关键字才能声明模版模版参数,但这并不意味着模版模版参数只能是 $class$ 模版。$struct$ 、$union$ 和别名模版同样可以。

3.5 等价性

        当所有入参都相等时,两个模版入参集合就是等价的。对于类型入参,类型别名并没有影响,比较依赖的是底层类型。对于整型非类参数,会进行值比较,不管值怎么表示。下面的例子说明了这个概念:

template<typename T, int I>
class Mix;

using Int = int;

Mix<int, 3 * 3>* p1;
Min<Int, 4 + 5>* p2;  // p2与p1类型相同

        在模版依赖上下文中,模版入参的值不是总能被确定,这时等价性的规则就会变得稍些复杂。例如:

template<int N> struct I {};

template<int M, int N> void f(I<M + N>);
template<int N, int M> void f(I<N + M>);

template<int M, int N> void f(I<N + M>);  // 错误

        在第二个和第三个声明中,虽然一个声明为 $N$ 和 $M$ ,一个声明为 $M$ 和 $N$ ,但因为在函数参数中都是第一个加第二个,所以它们还是等价的。最后一个声明中,函数参数中的加法顺序是颠倒的,这导致它与上面两个声明不等价。然而,因为这样还是会产生相同的结果,即函数等价,所以会导致一个错误。错误原因是只具有函数等价的表达式不是真正的等价。然而,你的编译器不需要处理这种错误,因为一些编译器可能会直接将类似于 $N + 1 + 1$ 的表达式优化为 $N + 2$ 。
        从函数模版中生成的与普通函数是永远不等价的,就算它们有着相同的名字和类型。对于类成员,这会产生两个后果:

  1. 从成员函数模版中生成的函数不会覆写虚函数;
  2. 从构造函数模版中生成的构造函数不会作为拷贝或者移动构造函数。类似的,从赋值模版中生成的函数也不会作为拷贝或者移动赋值运算符。

4. 变长模版

        变长模版是包含至少一个模版参数集合的模版。变长模版在一个模版可以从任意数量的入参生成时十分有用。当给变长模版指定入参时,每个模版参数集合会匹配一个或多个模版参数,我们称模版入参序列为入参集合。下面的例子说明了模版参数集合怎么匹配不同入参集合:

template<typename... Types>
class Tuple {
  // ...
};

int main() {
  Tuple<> t0;
  Tuple<int> t1;
  Tuple<int, float> t2;
}

        因为一个模版参数集合代表模版入参列表而不是一个单独的模版入参,它必须在结构支持入参集合中的所有参数的前提下使用。一个这样的结构是 $sizeof\dots$ 操作,它会返回入参集合中的参数数量:

template<typename... Types>
class Tuple {
public:
  static constexpr std::size_t length = sizeof...(Types);
};

int a1[Tuple<int>::length];
int a3[Tuple<short, int, long>::length];

4.1 集合展开

        $sizeof\dots$ 表达式是集合展开的一个例子。集合展开会把入参集合中的参数拆分为单独的参数。$sizeof\dots$ 只是返回单独参数的个数,其他形式的参数集合可以在需要的时候展开为多个参数。这种集合展开会在集合之后使用 $\dots$ 符号。例如:

template<typename... Types>
class MyTuple : public Tuple<Types...> {
  // ...
};

MyTuple<int, float> t2;

        模版参数 $Types\dots$ 是一个集合展开,生成一个模版参数序列。
        一种直观的理解集合展开的方式是把它们视为语法展开,模版参数集合被确切数量的模版参数代替,集合展开被写成单独入参的形式,每个入参对应一个非集合模版参数。两个参数的展开就类似于:

template<typename T1, typename T2>
class MyTuple : public Tuple<T1, T2> {
  // ...
};

        然而,注意你不能直接通过名字单独访问参数集合中的某个元素,因为它们并没有定义名字。如果你需要得到某个类型,你只能递归地将它们传递给另一个类或者函数。
        每个集合展开都具有模式,即每个入参的类型或者表达式,一般在 $\dots$ 之前。我们之前的例子只有最简单的模式,即获取参数集合元素的名字,其实模式可以更复杂。例如:

template<typename... Types>
class PtrTuple : public Tuple<Types*...> {
  // ...
};

PtrTuple<int, float> t3;

        这个模式使得 $PtrTuple$ 中的模版参数都变为指针类型。我们可以把它展开来:

template<typename T1, typename T2, typename T3>
class PtrTuple : public Tuple<T1*, T2*, T3*> {
  // ...
};

4.2 集合展开的时机

        集合展开可以在任意一个需要一个逗号分隔的列表的地方使用,包括:

        $sizeof\dots$ 是一个集合展开机制,但是不会真正产生一个列表。C++17也提供了折叠表达式,这也是一个不会产生列表的机制。有些集合展开场景只是为了完整性而提及,我们实际上只需要注意那些有用的场景。因为所有场景的集合展开都遵循相同的原则和语法,你也可以根据例子来扩展。
        在基类列表中使用集合展开通过混合 ( $mixins$ ) 聚合外部的数据和功能,这些类会混入类层级中,提供新行为。例如:

template<typename... Mixins>
class Point : public Mixins... {
  double x, y, z;

public:
  Point() : Mixins()... {}
  template<typename Visitor>
  void visitMixins(Visitor visitor) {
    visitor(static_cast<Mixins&>(*this)...);
  }
};

struct Color { char red, green, blue; };
struct Label { std::string name; };
Point<Color, Label> p;

        $Point$ 类使用集合展开来获取提供的混合类型,并在基类列表中展开。$Point$ 的默认构造函数会应用在基类初始化列表中应用集合展开来初始化每个基类。成员函数模版 $visitMixins$ 是最有趣的,它使用集合展开的结果作为参数来进行调用,将 $\star this$ 展开为对应的混合类型。
        集合展开也能在模版参数列表中创建非类或者模版参数集合:

template<typename... Ts>
struct Values {
  template<Ts... Vs>
  struct Holder {};
};

int i;
Values<char, int, int*>::Holder<'a', 17, &i> valueHolder;

        $Values$ 具有非类模版参数集合,每个模版参数都可以是不同类型。注意这里的 $\dots$ 有双重作用,既声明了模版参数集合,又进行集合展开。这种模版参数集合的使用很少见,相同的原理在函数参数中更常见。

4.3 函数参数集合

        一个函数参数集合可以匹配零个或多个函数调用参数。与模版参数集合一样,函数参数集合也使用 $\dots$ 前置的方式生命,后置的方式展开。模版参数集合和函数参数集合统称为参数集合。与模版参数集合不同的是,函数参数集合总是集合展开的,这意味着他们的声明类型必须包含至少一个参数集合。

template<typename... Mixins>
class Point : public Mixins... {
  double x, y, z;

public:
  Point(Mixins... mixins) : Mixins(mixins)... {}
};

struct Color { char read, green, blue; }
struct Label { std::string name; }
Point<Color, Label> p({0x7F, 0, 0x7F}, {"center"});

template<typename... Types>
void print(Types... values);

int main() {
  std::string welcom("Welcome to ");
  print(welcome, "C++ ", 2011, '\n');
}

        当调用 $print$ 时,参数会被放入参数集合里,使用 $Types$ 这个参数集合类型表示,实参使用 $values$ 表示。$print$ 函数的实际实现方式是递归模版实例化,这是一种模版元编程技术。
        在匿名函数参数集合和C风格的变长参数之间存在语法歧义,例如:

template<typename T> void c_style(int, T...);
template<typename... T> void pack(int, T...);

        在第一个例子中,$T\dots$ 被视为 $T,\dots$ ,即一个类型为 $T$ 的匿名参数,后面为C风格的变长参数;在第二个例子中,$T\dots$ 结构会被视为一个函数参数集合,因为$T$ 是一个有效的展开表达式。可以通过在 $\dots$ 前面添加逗号的方式来消除歧义。在泛型lambda中,如果类型使用 $auto$ 声明,那么后置的 $\dots$ 会被视为参数集合。

4.4 多重和内嵌集合展开

        集合展开可以很复杂,也可以是多重或者单独的参数集合。当实例化一个包含多重参数集合的集合展开时,所有参数集合必须拥有相同的长度。类型或者值的结果序列是通过使用每个集合的第一个元素替换,再使用第二个元素的替换……以此类推的方式,生成的。例如:

template<typename F, typename... Types>
void forwardCopy(F f, Types const&... values) {
  f(Types(values)...);
}

        调用参数集合展开为两个参数集合,$Types$ 和 $values$ 。当实例化这个模版时,逐元素地展开,产生一系列对象结构。在经过语法解释后,三个参数的 $forwardCopy$ 会类似于这种:

template<typename... F, typename T1, typename T2, typename T3>
void fowardCopy(F f, T1 const& v1, T2 const& v2, T3 const& v3) {
  f(T1(v1), T2(v2), T3(v3));
}

        集合展开也可以内嵌。这时每个参数集合会被离它最近的展开表达式展开。

template<typename... OuterTypes>
class Nested {
  template<typename... InnerTypes>
  void f(InnerTypes const&... innerValues) {
    g(OuterTypes(InnerTypes(innerValues)...)...);
  }
};

        在 $g$ 函数调用中,$InnerTypes(innerValuesi)$ 是最内层的集合展开,它们会生成一个序列,交给 $OuterTypes$ 继续展开。在经过语法解释后,一个例子如下:

template<typename O1, typename O2>
class Nested {
  template<typename I1, typename I2, typename I3>
  void f(I1 const& iv1, I2 const& iv2, I3 const& iv3) {
    g(O1(I1(iv1), I2(iv2), I3(iv3)),
      O2(I1(iv1), I2(iv2), I3(iv3)));
  }
};

4.5 零长度集合展开

        集合展开的语法解释对于理解变长模版在不同参数数量的情况下的行为很有帮助。然而,语法解释通常在零长度参数集合时会失败。为了说明这点,我们可以延用之前的 $Point$ 类声明:

template<>
class Point : {
  Point() : {}
};

        上面的写法是错误的,因为模版参数列表是空的,空的基类和基类初始化列表会导致只剩下一个冒号。集合展开是实际的语法构造,任意大小的参数集合替代不会影响解析。反而,当一个集合展开为一个空列表,程序行为会类似于没有提供列表。这个语义规则及时当解释一个零长度展开语法会

C++ Templates(7):深入模版基础