11-深入模板基础

参数化声明

  • C++现在支持四种原始模板:函数模板、类模板、C++14引入的变量模板(variable template)和别名模板(alias template)
template<typename T> // 类模板
class Data {
 public:
    static constexpr bool copyable = true;
};

template<typename T> // 函数模板
void f(T x) {}

template<typename T> // 变量模板
T zero = 0;

template<typename T> // 变量模板
bool dataCopyable = Data<T>::copyable;

template<typename T> // 别名模板
using DataList = Data<T*>;
  • 注意静态数据成员Data<T>::copyable不是一个变量模板,只是通过类模板Data间接参数化。但变量模板可以作为静态成员模板出现在类作用域。下面是在类内定义四种模板的例子
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变量,变量(包括静态数据成员)和变量模板能被内联,这意味着它们的定义能跨编译单元。对于总能定义在多个编译单元中的变量模板来说,这是多余的。但类内定义的静态数据成员不会像成员函数一样内联,因此就要指定inline关键字
template<int I>
class CupBoard
  ...
  inline static double totalWeight = 0.0;
};
  • 下面是类外定义成员模板的例子
template<typename T>
class List {
 public:
  List() = default;

  template<typename U>
  class Handle;

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

  template<typename U>
  static U zero;
};

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

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

template<typename T>
  template<typename U>
U List<T>::zero = 0;
  • union模板(通常被看作类模板的一种)
template<typename T>
union AllocChunk {
  T object;
  unsigned char bytes[sizeof(T)];
};
  • 函数模板同普通函数一样,可以有默认实参
template<typename T>
void report_top(const Stack<T>&, int number = 10);

template<typename T>
void fill(Array<T>&, const T& = T{}); // C++11前写为T()

虚成员函数

  • 成员函数模板不能为虚函数,因为虚函数表的大小是固定的,而成员函数模板的实例化个数要编译完成后才能确定。非模板成员可以为虚函数,因为类被实例化后成员数量是固定的
template<typename T>
class Dynamic {
 public:
  virtual ~Dynamic(); // 正确:每个Dynamic<T>对应一个析构函数

  template<typename U>
  virtual void copy(const U&); // 错误:编译器不知道一个Dynamic<T>中copy()个数
};

模板的链接(Linkage of Template)

  • 除了函数模板的重载,每个模板在其作用域中必须有一个唯一的名称,类模板不能和其他实体共用一个名称
int C;
class C; // OK:两者名称在不同的空间

int X;
template<typename T>
class X; // 错误:名称冲突

struct S;
template<typename T>
class S; // 错误:名称冲突
  • 模板名称是具有链接的,但不能有C链接(C linkage)
extern "C++" template<typename T>
void normal(); // 默认方式,上面的链接规范可以省略不写

extern "C" template<typename T>
void invalid(); // 错误:不能使用C链接

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;

原始模板(Primary Template)

  • 如果模板声明是一个普通声明(没有在模板名称后添加尖括号),这个声明就是一个原始模板
template<typename T> class Box;        // OK: primary template
template<typename T> class Box<T>;       // ERROR: does not specialize

template<typename T> void translate(T);    // OK: primary template
template<typename T> void translate<T>(T);   // ERROR: not allowed for functions

template<typename T> constexpr T zero = T{};   // OK: primary template
template<typename T> constexpr T zero<T> = T{};  // ERROR: does not specialize
  • 声明类的偏特化或变量模板时,声明的就是非原始模板
  • 函数模板必须是原始模板

模板参数

  • 模板参数有三种:类型参数、非类型参数、模板的模板参数,类型参数就是最常见的用法
  • 在声明后不会引用模板参数名称时,模板参数的名称可以省略不写
template<typename, int> // 省略T
class X;
  • 在模板声明后需要引用参数名称时,则必须写上模板参数名称
template<typename T, T Root, template<T> class Buf>
class Structure;

非类型参数(Nontype Parameter)

  • 非类型参数表示在编译期或链接期可以确定的常值,必须是以下的一种
    • 整型或枚举类型
    • 指针类型
    • 左值引用类型
    • std::nullptr_t
    • 包含auto或decltype(auto)类型(C++17)
  • 在非类型参数的声明前使用关键字typename是为了指明受限的名称
template<typename T, // 类型参数
  typename T::Allocator* Allocator> // 非类型参数
class List;
  • 函数和数组类型也可以被指定为非类型参数,但会退化为指针类型
template<int buf[5]> class Lexer;
template<int* buf> class Lexer; // OK:重复声明

template<int fun()> struct FuncWrap; 
template<int (*)()> struct FuncWrap; // OK:重复声明
  • 非类型参数不能用static、mutable修饰,可以用cv限定符修饰,但如果cv限定符是最外层的参数类型,会被编译器忽略
template<const int const length> class Buffer; // const会被忽略
  • 在表达式中使用时,非引用的非类型参数总是纯右值(prvalue),不能被寻址或赋值,而左值引用的非类型参数能表示一个左值
template<int& Counter>
struct LocalIncrement {
  LocalIncrement() { Counter = Counter + 1; }   // OK:一个指向int的引用
  ~LocalIncrement() { Counter = Counter - 1; }
};

模板的模板参数(Template Template Parameter)

  • 模板的模板参数的声明和类模板类似,但不能用关键字struct和union
template<template<typename X> class C> // OK
void f(C<int>* p);

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

template<template<typename X> union C> // 错误:不能用union
void f(C<int>* p);
  • C++17中允许使用typename代替class
template<template<typename X> typename C> // OK since C++17
void f(C<int>* p);
  • 模板的模板参数中的参数也可以有默认实参
template<template<typename T,
  typename A = MyAllocator> class Container>
class Adaptation {
  Container<int> storage; // Container<int, MyAllocator>
  ...
};
  • 模板的模板参数的参数名称只能被自身其他参数的声明使用
template<template<typename T, T*> class Buf>  // OK
class Lexer {
  static T* storage;  // 错误:模板的模板参数不能用在此处
  ...
};
  • 通常模板的模板参数的名称不会在后面被用到,所以一般可以省略不写
template<template<typename, // 省略T
  typename = MyAllocator> class Container>
class Adaptation {
  Container<int> storage; // Container<int, MyAllocator>
  ...
};

模板参数包(Template Parameter Pack)

  • C++11开始,任何类型的模板参数都能转换为一个模板参数包
template<typename... Types>
class 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();
  • 原始类模板、变量模板和别名模板可能有至多一个模板参数包,如果存在则必须作为最后一个模板参数。函数模板可以有多个模板参数包,只要每个跟在参数包后的模板参数有一个默认值,或者能被推断
template<typename... Types, typename Last>
class LastType; // 错误:模板参数包必须为最后一个参数

template<typename... TestTypes, typename T>
void runTests(T value); // OK:跟在参数包后的T能被推断

template<unsigned...> struct Tensor;
template<unsigned... Dims1, unsigned... Dims2>
auto compose(Tensor<Dims1...>, Tensor<Dims2...>); // OK:Dims1和Dims2能被推断
  • 类的偏特化和变量模板的声明能有多个参数包
template<typename...> Typelist;
template<typename X, typename Y> struct Zip;
template<typename... Xs, typename... Ys>
struct Zip<Typelist<Xs...>, Typelist<Ys...>>; // OK:偏特化用推断替代Xs和Ys
  • 类型参数包不能被其他参数使用
template<typename... Ts, Ts... vals> // 错误:Ts不能用于声明其他参数
struct StaticValues {};
  • 但嵌套的模板可以使用外层模板的参数包
template<typename... Ts>
struct ArgList {
  template<Ts... vals>
  struct Vals {};
};

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

默认模板实参

  • 除参数包外的任意模板参数都可以有默认实参,默认实参可以依赖于前面的参数
template<typename T, typename Allocator = allocator<T>>
class List;
  • 如果类模板、变量模板、别名模板指定了一个默认实参,之后所有的参数都要有默认实参
template<typename T1, typename T2, typename T3,
  typename T4 = char, typename T5 = char>
class X; // OK

template<typename T1, typename T2, typename T3 = char,
  typename T4, typename T5>
class X; // 正确:T4和T5在前面已经有了默认值

template<typename T1 = char, typename T2, typename T3,
  typename T4, typename T5>
class X; // 错误: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() {} // OK
};

template<typename>
void g(); // 错误:g()在定义中给出了默认实参,这里不能有其他声明

模板实参

  • 模板实参指实例化模板时用来替换模板参数的值,可以用下列几种机制在确定
    • 显式模板实参:紧跟在模板名称后在一堆尖括号内部的显式模板实参值,所组成的完整名称称为template-id
    • 注入式类名称:在带有参数P1,P2…的类模板X作用域中,模板名称(即X)等同于template-id(即X<P1, P2, ...>
    • 默认模板实参:如果提供了默认模板实参就可以省略显式模板实参
    • 实参推断:如果所有的模板实参都可以通过推断获得就可以省略显式模板实参

函数模板实参

  • 函数模板实参既可以显式指定也可以隐式推断
template<typename T>
T max(T a, T b)
{
  return a < b ? b : a;
}

int main()
{
  ::max<double>(1.0, -3.0); // 显式指定
  ::max(1.0, -3.0); // 隐式推断
  ::max<int>(1.0, 3.0); // 指定为int抑制推导
}
  • 不能推断的模板参数放在最前面,这样只要显式指定这些参数,其他参数仍可隐式推断
// DstT未出现在参数列表中,无法被推导
template<typename DstT, typename SrcT>
DstT implicit_cast(const SrcT& x)
{
  return x;
}

int main()
{
  double value = implicit_cast<double>(-1); // 只需指定一个参数
}
  • 由于函数模板可以被重载,显式指定所有实参可能会定义一系列函数而非单个函数
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); // OK
  apply(&multi<int>, 7); // 错误:没有单个multi<int>
}
  • 显式指定模板实参可能导致构造一个无效的C++类型,参考下列代码,test<int>对第一个函数模板没有意义,因为int类型没有成员类型X,而第二个表达式没有此问题,因此&test<int>唯一标识第二个函数的地址,尽管第一个模板用int替换失败,但没有造成&test<int>非法
template<typename T> RT1 test(const typename T::X*);
template<typename T> RT2 test(...);
  • 这就能为不同实参确定不同的返回类型
using RT1 = char;
using RT2 = struct { char a[2]; };
template<typename T> RT1 test(const typename T::X*);
template<typename T> RT2 test(...);
// 在编译期判断给定类型T是否具有成员类型X
#define type_has_member_type_X(T) (sizeof(test<T>(0)) == 1)
  • SFINAE原则只是防止创建非法类型,并不能防止非法算式
template<int I>
void f(int (&) [24/(4-I]);

template<int I>
void f(int (&) [24/(4+I]);

int main()
{
  &f<4>; // 错误:替换后第一个除数为0,未使用SFINAE原则
}
  • 这个错误出现在求值过程中,而非编译器把算式值绑定到模板实参时,下例合法
template<int N>
int g() { return N; }

template<int* P>
int g() { return *p; }

int main()
{
  return g<1>(); // 1不能被绑定到int*,应用了SFINAE原则
}

类型实参

  • C++11之前,局部类和局部枚举、unnamed类类型或枚举类型(即内部数据成员未定义的类或枚举)不能作为类型实参
template<typename T>
class List {
  ...
};

typedef struct {
  double x, y, z;
} Point;

typedef enum { red, green, blue } *ColorPtr;

int main()
{
  struct Association {
    int* p;
    int* q;
  };
  List<Association*> error1; // 错误:不能是局部类型
  List<ColorPtr> error2; // 错误:不能是unnamed type
  List<Point> ok; // 正确:无名的type因typedef有了名称
}
  • C++11后为任意类型都可以作为模板实参,只要该类型替换模板参数后获得的构造有效
template<typename T>
void clear(T p)
{
  *p = 0; // 要求运算符*可用于类型T
}

int main()
{
  int a;
  clear(a); // 错误:int不支持*
}

非类型实参

template<typename T, T nontype_param>
class C;

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

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

void f();
void f(int);
C<void(*)(int), f>* c3; // 匹配f(int),f前的&省略

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,然后由int转为long
  • 模板实参的一个普遍约束是,必须能在编译期或链接期确定实参值,运行期才能确定实参值(如局部变量地址)就不符合模板在程序创建时才实例化的概念。非类型实参不能是空指针常量(C++11前)、浮点数、字符串字面值
  • 不能用字符串字面值作为非类型模板实参的技术原因是,两个内容一样的字符串可能存在两个不同地址中。一种笨拙的解决方法是引入一个额外的变量存储这个字符串。非类型模板参数声明为引用或指针的要求是,在所有版本中可以是一个有外部链接的常量表达式,在C++11中则是内部链接,在C++17中则可以是任意链接
template<const char* 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;   // OK in all versions
  Message<hello11> msg11;   // OK since C++11
  Message<hello17> msg17;   // OK since 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; // 错误:不能使用数组内某个元素的地址

模板的模板实参

  • 模板的模板实参必须是一个类模板或别名模板,C++17前要求精确匹配其本身的模板参数,默认模板实参会被编译器忽略,C++17放宽了匹配规则
#include <list>
  // declares in namespace std:
  // template<typename T, typename Allocator = allocator<T>>
  // class list;

template<typename T1, typename T2,
  template<typename> class Cont>  // Cont期望一个参数
class Rel {
  ...
};

Rel<int, double, std::list> rel;  // 错误(C++17前):std::list有超过一个模板参数
  • 老的解决方法是给模板的模板参数添加一个有默认值的参数,这样做虽然并不完善,但可以让标准容器模板得到使用
template<typename T1,
  typename T2,
  template<typename T,
    typename = std::allocator<T>> class Container>
class Rel {
  ...
};
  • 变长的模板的模板参数不需要遵循C++17前的精确匹配规则
template<typename T1, typename T2,
  template<typename... > class Cont>
class Rel {
  ...
};

Rel<int, double, std::list> rel;
  • 模板参数包只能匹配同类型的模板实参
#include <list>
#include <map>
  // declares in namespace std:
  //  template<typename Key, typename T,
  //       typename Compare = less<Key>,
  //       typename Allocator = allocator<pair<Key const, T>>>
  //  class map;
#include <array>
  // declares in namespace std:
  //  template<typename T, size_t N>
  //  class array;

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

AlmostAnyTmpl<std::vector> withVector; // 两个类型参数
AlmostAnyTmpl<std::map> withMap; // 四个类型参数
AlmostAnyTmpl<std::array> withArray; // 错误:一个参数包不能同时匹配类型和非类型参数
  • C++17前模板的模板参数只能用class关键字修饰,现在也可以用typename修饰

实参的等价性

  • 当两组模板实参对应的元素都相等时,这两组模板实参等价。对于非类型整型实参,比较的是实参的值,与表达方式无关
template<typename T, int I>
class Mix;

using Int = int;

Mix<int, 3*3>* p1;
Mix<Int, 4+5>* p2; // p2和p1类型相同
  • 在模板依赖的上下文中,模板实参的值不是总能确定地建立,等价性的规则变得稍微复杂一些
template<int N> struct I {};

template<int X, int Y> void f(I<X+Y>);  // #1
template<int Y, int X> void f(I<Y+X>);  // #2
template<int X, int Y> void f(I<Y+X>);  // #3 ERROR
  • 1和2的声明是等价的,但3则不是,但在被调用时会产生同样的结果,因此3是功能等价的(functionally equivalent)。功能等价不是真的等价,以不同的方式声明模板是一个错误,但一些编译器内部可能把N+1+1视为和N+2等价,所以标准在这里放宽了约束,不需要一个具体的实现方法
  • 从函数模板实例化而来的函数永远不和普通函数等价,这样对类成员有两个重要影响
    • 由成员函数模板实例化的函数不会重写虚函数
    • 由构造函数模板实例化的构造函数一定不会是一个拷贝或移动构造函数,同理由赋值模板产生的赋值运算符也不会是一个拷贝或移动赋值运算符(这个问题较少出现,因为拷贝或移动赋值运算符不会被隐式调用)

可变参数模板(Varaidic Template)

  • 可变参数模板是包含至少一个模板参数包的模板
template<typename... Types>
class Tuple {
  // provides operations on the list of types in Types
};

int main()
{
  Tuple<> t0;
  Tuple<int> t1;
  Tuple<int, float> t2;
}
  • 实参包的实参必须有相同的构造,sizeof...操作就是这样一个构造,作用是计算实参包的实参数量
template<typename... Types>
class Tuple {
 public:
  static constexpr std::size_t length = sizeof...(Types);
};

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

包扩展(Pack Expansion)

  • 包扩展是一个把实参包到分离成实参的构造,sizeof...表达式就是一个包扩展的例子,包扩展的定义方法是在列表中的元素右边加上省略号
template<typename... Types>
class MyTuple : public Tuple<Types...> { // 模板实参Types...就是一个包扩展
  // extra operations provided only for MyTuple
};

MyTuple<int, float> t2;  // 派生自Tuple<int, float>
// int、float替换给参数包Types,Types...包扩展后又得到int和float实参
  • 一个直观的理解包扩展的方法是,把它们视为一个语法扩展,比如如果有两个参数,MyTuple相当于
template<typename T1, typename T2>
class MyTuple : public Tuple<T1, T2> {
  // extra operations provided only for MyTuple
};
  • 有三个参数则是
template<typename T1, typename T2, typename T3>
class MyTuple : public Tuple<T1, T2, T3> {
  // extra operations provided only for MyTuple
};
  • 但是不能直接通过名字访问参数包的单个元素,如果需要这些类型,只能把它们传给另一个类或函数
  • 每个包扩展有一个模式(pattern),也就是参数包的每个实参重复的类型,即Types…的模式是参数包的名字Types,但模式也可以复杂化
template<typename... Types>
class PtrTuple : public Tuple<Types*...> {
  // extra operations provided only for PtrTuple
};

PtrTuple<int, float> t3; // 派生自Tuple<int*, float*>
  • 包扩展Types*...的模式是Types*,把每个元素用模式扩展后就得到一个实参序列,每个实参的类型都是Type*,如果有三个参数,PtrTuple就是下面这样
template<typename T1, typename T2, typename T3>
class PtrTuple : public Tuple<T1*, T2*, T3*> {
  // extra operations provided only for PtrTuple
};

包扩展出现的位置

  • 包扩展基本可以在任何提供了一个逗号分割的列表中使用
    • 基类列表
    • 基类构造函数的初始化列表
    • 调用实参的列表(模式是实参表达式)
    • 初始化列表(如括号初始化列表)
    • 类或函数或别名模板的模板参数列表
    • 函数抛出的异常列表(C++11/14弃用,C++17不允许)
    • 在一个属性(attribute)中,如果属性本身支持包扩展(C++标准中没有定义这样的attribute)
    • 指定一个声明的alignment
    • 指定lambda的捕获列表
    • 函数类型的参数列表
    • using声明中(C++17)
  • 基类列表中的包扩展可以扩展一些直接基类
template<typename... Mixins>
class Point : public Mixins... { // 基类包扩展
 public:
  Point() : Mixins()... { } // 基类初始化列表包扩展

  template<typename Visitor>
  void visitMixins(Visitor visitor)
  {
    visitor(static_cast<Mixins&>(*this)...); // 调用实参包扩展
  }
 private:
  double x, y, z;
};

struct Color { char red, green, blue; };
struct Label { std::string name; };
Point<Color, Label> p; // 派生自Color和Label
  • 包扩展也能用于模板参数列表来创建一个非类型或模板参数包
template<typename... Ts>
struct Values {
  template<Ts... Vs>
  struct Holder {};
};

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

函数参数包

  • 一个函数参数包是一个匹配任意数量函数调用实参的函数参数,模板参数包和函数参数包统称为参数包
  • 不同于模板参数包,函数参数包总是包扩展,因此它们的声明类型必须包含至少一个参数包
template<typename... Mixins>
class Point : public Mixins... {
 public:
    Point(Mixins... mixins) // mixins是一个函数参数包
  : Mixins(mixins)... { }  // 用mixin值初始化每个基类
 private:
  double x, y, z;
};

struct Color { char red, 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 welcome("Welcome to ");
  print(welcome, "C++", 2011, '\n'); // 调用print<std::string, const char*, int, char>
}
  • 参数列表末尾出现的未命名的函数参数包,和C风格的可变参数会产生歧义
template<typename T> void c_style(int, T...); // T... 被看成 T, ...
template<typename... T> void pack(int, T...); // T...是函数参数包
  • 在lambada中,如果紧接在…前(中间没有逗号)的类型包含auto,…就会被视为参数包

多重嵌套包扩展(Multiple and Nested Pack Expansion)

  • 当实例化包含多个参数包的包扩展时,所有的参数包必须具有相同的长度。依次将每个参数包的第一个实参替换为模式,接着是每个参数包的第二个实参,从而生成一个类型或值的序列
template<typename F, typename... Types>
void forwardCopy(F f, const Types&... args)
{
  f(Types(args)...);
}
  • 三个实参时如下
template<typename F, typename T1, typename T2, typename T3>
void forwardCopy(F f, const T1& v1, const T2& v2, const T3& v3)
{
  f(T1(v1), T2(v2), T3(v3));
}
  • 包扩展本身也可能嵌套,每个参数包由它最近的包扩展来扩展,下面是调用三个不同参数包的嵌套包扩展
template<typename... T>
class Nested {
  template<typename... U>
  void f(const U&... x)
  {
    g(T(U(x)...)...);
  }
};
  • 当T有两个实参,U和x都有三个实参时,相当于
template<typename T1, typename T2>
class Nested {
  template<typename U1, typename U2, typename U3>
  void f(const U1& x1, const U2& x2, const U3& x3)
  {
    g(T1(U1(x1), U2(x2), U3(x3)), T2(U1(x1), U2(x2), U3(x3)));
  }
};

零长包扩展(Zero-Length Pack Expansion)

  • 零长实参包的语法解释经常会失败。下面这个零实参的类的语法是错误的
template<>
class Point : {
  Point() : {}
};
  • 但包扩展到最后却不会出错,因为包扩展实际上是一种语义结构,任何大小的参数包的替换都不影响包扩展的解析。当包扩展到空列表时,只是表现得像列表不存在,但语义上是正确的
template<typename T, typename... Types>
void g(Types... args)
{
  T x(args...);
}
  • 模板中创建一个值x,如果参数包序列为空,x的声明看起来就是T x(),然而因为包扩展的替换是符合语义的,不会影响语法分析,x就会由零个实参初始化

折叠表达式(Fold Expression)

  • 一个递归的模式是一个值序列操作的折叠,如果没有这个特性,就可能要这样实现operator&&
bool and_all() { return true; }

template<typename T>
bool and_all(T cond)
{
  return cond;
}

template<typename T, typename... Ts>
bool and_all(T cond, Ts... conds)
{
  return cond && and_all(conds...);
}
  • C++17中加入了折叠表达式的特性,可用于除了.->[]外的所有二元操作符
  • 给定一个未扩展的表达式牧师包和一个非模式表达式值,C++17允许写出如下operator op,注意圆括号不能省略
(pack op ... op value) // 二元右折叠
(value op ... op pack) // 二元左折叠
  • 使用这个特性,下面代码
template<typename... T>
bool g()
{
  return and_all(trait<T>()...);
}
  • 可重写为
template<typename... T>
bool g()
{
  return (trait<T>() && ... && true);
}
  • 折叠表达式是包扩展,如果包是空的,折叠表达式类型仍能由操作数(value)决定,但这个特性的设计者还希望能省略value,因此C++17还允许另外两种写法
(pack op ...) // 一元右折叠
(... op pack) // 一元左折叠
  • 而对于空扩展需要决定类型和值,空的一元折叠表达式通常会产生错误,除了三种例外情况
    • 一个&&的一元折叠的空扩展产生值true
    • 一个||的一元折叠的空扩展产生值false
    • 一个,的一元折叠空扩展产生一个void表达式
  • 但如果重载这些特殊的操作符,也可能产生预期之外的结果
struct X {
  ...
};

X operator||(X, X);

template<typename... T>
void f(T... args)
{
  X value = (args || ...);
  ...
}
  • 假如以继承自X的类型调用模板,其他所有扩展产生一个X值,而最终对空扩展将产生一个bool值。因此通常应该避免使用一元折叠而使用二元折叠

友元类

  • 如果把类模板的实例声明为友元,该友元类模板必须已经声明并可见。而普通类作为友元则没有此要求
template<typename T>
class Node;

template<typename T>
class Tree {
  friend class Node<T>; // 类模板Node必须在此前被声明并可见
  friend class X; // OK:即使其他地方没有声明此类
};
  • 一个应用是将其他类模板实例声明为友元,以获取友元对象所有成员的访问权限
template<typename T>
class Stack {
 public:
  template<typename T2>
  Stack<T>& operator=(const Stack<U>&);
  // to get access to private members of Stack<U> for any type U:
  template<typename>
  friend class Stack;
};
  • C++11允许让一个模板参数为友元
template<typename T>
class Wrap {
  friend T; // 对任意类型T有效,但如果T不是类类型则被忽略
};

友元函数

  • 函数模板实例也可以成为友元,但不能在友元声明中定义模板实例,因为实例是由编译器生成的
template<typename T1, typename T2>
void f(T1, T2);

class Mixer {
  friend void f<>(int&, int&); // T1 = int&, T2 = int&
  friend void f<int, int>(int, int); // T1 = int, T2 = int
  friend void f<char>(char, int); // T1 = char, T2 = int
  friend void f<>(long, long) { ... } // 错误:不能在此定义
};
  • 如果友元名称后没有紧跟一对尖括号只有两种情况:如果名称不是受限的(未被::指定)则匹配普通函数,如果函数未出现过,这个友元声明可以是函数的首次声明或定义;如果名称是受限的(被::指定)就必须引用一个已定义的函数或函数模板,并优先匹配普通函数,这个友元声明不能是定义
void f(void*); // 普通函数

template<typename T>
void f(T);

class X {
  friend void f(int) {} // 定义了一个新函数::f(int)
  friend void ::f(void*); // 引用上面的普通函数
  friend void ::f(int); // 引用一个模板实例
  friend void ::f<double*>(double*); // 受限名称可以带尖括号,但模板在此必须可见
  friend void ::error() {} // 错误:受限的友元不能是定义式
};
  • 类模板的模板参数可以用来标识友元函数
template<typename T>
class Node {
  Node<T>* allocate();
};

template<typename T>
class List {
  friend Node<T>* Node<T>::allocate();
};
  • 在类模板内部定义友元函数存在一个问题,友元函数的实体在类模板实例化之前是不存在的
template<typename T>
class X {
  friend void f() {} // 定义一个新函数::f(),X实例化后该函数才存在
};

int main()
{
  X<void> miracle; // ::f()此时被生成
  X<double> oops; // 错误:第二次生成::f()
}
  • 类模板外定义则没有此问题
template<typename T>
class X {
  friend void f();
};

void f() {}

int main()
{
  X<void> miracle; // OK
  X<double> oops; // OK
}
  • 如果想在类模板内定义友元函数,则必须确保友元函数中包含类模板的模板参数,以使每个类模板实例都生成了一个不同的函数
template<typename T>
class X {
  friend void f(X<T>) {}  // 每个T实例化一个不同的函数::f()
};

int main()
{
  X<void> a;
  f(a); // 实例化::f(X<void>)
  X<double> b;
  f(b); // 实例化::f(X<double>)
  X<void> c;
  f(c); // 已实例化过::f(X<void>),直接调用
}
  • 虽然这些函数是作为模板的一部分被生成的,但本身仍是普通函数而不是模板的实例
  • 由于这些函数定义在类中,它们都会被隐式内联,因此在不同的编译单元可以生成相同的函数

友元模板

  • 友元模板可以让模板的所有实例都成为友元,它只能声明原始模板和原始模板的成员,但与该原始模板相关的所有特化也都会被视为友元。和普通友元一样,友元函数模板是非受限(未被::指定)名称且后面没有紧跟尖括号时,友元模板才能成为定义
class X {
  template<typename T> friend class A;

  template<typename T>
  friend void B<T>::g(A<T>*);

  template<typename T>
  friend int f()
  {
    return ++X::n;
  }

  static int n;
};

文章作者: 张小飞
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 张小飞 !
 上一篇
12-模板中的名称 12-模板中的名称
ADL(Argument-Dependent Lookup,Koenig Lookup) 下面例子解释了名称查找的基本规则 namespace A { struct X; struct Y; void f(int); void g(X);
下一篇 
10-泛型库 10-泛型库
回调 回调的含义是:对一个库,用户希望库能够调用用户自定义的某些函数,这种调用称为回调。C++中用于回调的类型统称为函数对象类型,它们能直接用作函数实参 #include <iostream> #include <vector
  目录