第12章 深入模板基础

在本章中,我们将深入探讨本书第一部分中介绍的一些基础知识:模板的声明,模板参数(template paramenters)的限制(restrictions),模板实参(template arguments)的限制(constraints)等等。

12.1 参数化声明

C++目前支持4种基础模板:类模板、函数模板、变量模板以及别名模板。每一种模板都既可以出现在命名空间作用域,也可以出现在类作用域。在类作用域中,它们作为嵌套的类模板、成员函数模板、静态数据成员模板以及成员别名模板。此类模板的声明与普通类、函数、变量以及类型别名(或者是它们的类成员副本)非常相似,只不过需要一个形如template<parameters here>的子句来做前置指引。

注意到C++17引入了另一种带有这样的参数化子句的结构:推导指引(deduction guides)(参考P42节2.9以及P314节15.12.1)。本书中它们不被称为模板(因为它们没有被实例化),但是这一语法的选择会让人联想到函数模板。

在下一节中,我们将重返实际模板参数声明。首先,一些示例用以说明四种类型的模板。它们可以像这样在命名空间作用域(全局或是某个命名空间内)中出现:

details/definitions1.hpp

  1. template<typename T> // a namespace scope class template
  2. class Data {
  3. public:
  4. static constexpr bool copyable = true;
  5. };
  6. template<typename T> // a namespace scope function template
  7. void log (T x) {
  8. }
  9. template<typename T> // a namespace scope variable template (since C++14)
  10. T zero = 0;
  11. template<typename T> // a namespace scope variable template (since C++14)
  12. bool dataCopyable = Data<T>::copyable;
  13. template<typename T> // a namespace scope alias template
  14. using DataList = Data<T*>;

注意到示例中,静态数据成员Data<T>::copyable并不是一个变量模板,尽管它是通过类模板Data参数所间接参数化的。然而,变量模板可以出现在类作用域中(下一个例子会展示),彼时它将作为一个静态数据成员模板。

下面展示了定义在所属类中的4种模板,它们都是类的成员:

details/definitions2.hpp

  1. class Collection {
  2. public:
  3. template<typename T> // an in-class member class template definition
  4. class Node {
  5. ...
  6. };
  7. template<typename T> // an in-class (and therefore implicitly inline)
  8. T* alloc() { // member function template definition
  9. ...
  10. }
  11. template<typename T> // a member variable template (since c++14)
  12. static T zero = 0;
  13. template<typename T> // a member alias template
  14. using NodePtr = Node<T>*;
  15. };

注意到在C++17中,变量(包括静态数据成员)以及变量模板都可以是内联的,内联意味着它们的定义可以跨越多个编译单元重复。对于总能定义在多个编译单元中的变量模板来说,这是多余的。但类内定义的静态数据成员不会像成员函数一样内联,因此就要指定inline关键字。

最后,下面的代码演示了如何在类外定义别名模板以外的成员模板:

details/definitions3.hpp

  1. template<typename T> // a namespace scope class template
  2. class List {
  3. public:
  4. List() = default; // because a template constructor is defined
  5. template<typename U> // another member class template,
  6. class Handle; // without its defination
  7. template<typename U> // a member function template
  8. List (List<U> const&); // (constructor)
  9. template<typename U> // a member variable template (since C++14)
  10. static U zero;
  11. };
  12. template<typename T> // out-of-class member class template definition
  13. template<typename U>
  14. class List<T>::Handle {
  15. ...
  16. };
  17. template<typename T> // out-of-class member function template definition
  18. template<typename T2>
  19. List<T>::List(List<T2> const& b)
  20. {
  21. ...
  22. }
  23. template<typename T> // out-of-class static data member template definition
  24. template<typename U>
  25. U List<T>::zero = 0;

定义在类外的成员模板需要多个template<... >参数化子句:每个外围作用域的类模板一个,成员模板本身也需要一个。子句从类模板最外层开始逐行展示。

同时也注意到构造器模板(一种特殊的成员函数模板)会禁用掉隐式声明的默认构造器(因为只有在没有其他构造器被声明时,默认构造器才会被声明)。增加一个默认的声明:

  1. List() = default;

这确保了List<T>的实例可以通过隐式声明的默认构造器构造出来。

联合体模板 联合体模板(union templates)也是可行的(它们被视为一种类模板):

  1. template<typename T>
  2. union AllocChunk {
  3. T object;
  4. unsigned char bytes[sizeof(T)];
  5. };

默认调用参数 函数模板可以有默认参数,就如同普通的函数一样:

  1. template<typename T>
  2. void report_top(Stack<T> const&, int number = 10);
  3. template<typename T>
  4. void fill(Array<T>&, T const& = T{}); // T{} is zero for built-in types

第二个声明展示了默认调用参数可以依赖于模板参数。它也可以被定义成如下形式(在C++11之前唯一可行的方式,可以参考P68节5.2):

  1. template<typename T>
  2. void fill(Array<T>&, T const& = T()); // T() is zero for built-in types

fill()函数被调用时,如果传入了第二个参数,那么默认参数不会实例化。这保证了如果默认调用参数对特定T无法实例化的情景下不会发生错误。例如:

  1. class Value {
  2. public:
  3. explicit Value(int); // no default constructor
  4. };
  5. void init(Array<Value>& array)
  6. {
  7. Value zero(0);
  8. fill(array, zero); // OK: default constructor not used
  9. fill(array); // ERROR: undefined default constructor for Value is used
  10. }

类模板的非模板成员 除了类内定义的4种基础模板以外,你还可以定义普通的类成员作为类的一部分。它们有时(错误地)也称为成员模板(member templates)。尽管它们可以被参数化,但这种定义并非是第一类模板(指上述的几种模板)。它们的参数完全由成员所在的模板本身决定。例如:

  1. template<int I>
  2. class CupBoard
  3. {
  4. class Shelf; // ordinary class in class template
  5. void open(); // ordinary function in class template
  6. enum Wood : unsigned char; // ordinary enumeration type in class template
  7. static double totalWeight; // ordinary static data member in class template
  8. };

对应的定义仅仅只是为所属的类模板指定了参数化子句,但是却并没有为成员本身指定,因为其并非是一个模板(没有参数化子句与最后一个::之后出现的名称相关联)。

  1. template<int I> // definition of ordinary class in class template
  2. class CupBoard<I>::Shelf {
  3. ...
  4. };
  5. template<int I> // definition of ordinary function in class template
  6. void CupBoard<I>::open()
  7. {
  8. ...
  9. }
  10. template<int I> // definition of ordinary enumeration type class in class template
  11. enum CupBoard<I>::Wood {
  12. Maple, Cherry, Oak
  13. };
  14. template<int I> // definition of ordinary static member in class template
  15. double CupBoard<I>::totalWeight = 0.0;

C++17之后,静态成员totalWeight可以在类模板内部使用inline关键字初始化。

  1. template<int I>
  2. class CupBoard {
  3. ...
  4. inline static double totalWeight = 0.0;
  5. };

尽管这种参数化定义通常被称作模板,但这里的“模板”一词相当不合适。对于这种情况,有一个经常被推荐的词是”temploid”。C++17之后,C++标准定义了模板化实体(a templated entity)的概念,它包括templates和temploids,以及递归地包含模板化实体中创建或定义的任何实体(这包括,例如,一个类模板内定义的友元函数(参考P30节2.4)或是模板中出现的一个lambda表达式闭包)。不管是temploid还是templated entity目前都没有产生足够的吸引力,但是在未来,需要更精准的沟通C++模板时,这些术语可能会很有用。

12.1.1 虚成员函数

成员函数模板不能被声明为virtual。施加这一限制是因为虚函数调用机制的通用实现会使用一个固定大小的虚表,其中存储了每一个虚函数条目(译者注:虚函数指针)。然而,成员函数模板直到整个程序被编译之前,实例化的个数都无法固定。因此,成员函数模板支持virtual需要C++编译器和链接器支持一种全新的机制。

相反的,类模板的普通成员函数可以是virtual,因为它们的数量是固定的。

  1. template<typename T>
  2. class Dynamic {
  3. public:
  4. virtual ~Dynamic(); // OK: one destructor per instance of Dynamic<T>
  5. template<typename T2>
  6. virtual void copy(T2 const&); // ERROR: unknown number of instances of copy()
  7. // given an instance of Dynamic<T>
  8. };

12.1.2 模板的链接

每个模板都必须有一个名字,并且该名字必须是所属作用域内独一无二的,除了函数模板重载的情景(参考第16章)。特别要注意,与类类型不同,类模板无法与不同类型的实体共享名称:

  1. int C;
  2. ...
  3. class C; // OK: class names and nonclass names are in a different "space"
  4. int X;
  5. ...
  6. template<typename T>
  7. class X; // ERROR: conflict with variable X
  8. struct S;
  9. ...
  10. template<typename T>
  11. class S; // ERROR: conflict with struct S

模板名称具有链接,但是他们无法拥有C链接。非标准链接可能具有某个依赖于实现体的意义(然而我们并不知道某个实现体支持模板的非标准链接与否):

  1. extern "C++" template<typename T>
  2. void normal(); // this is the default: the linkage specification could be left out
  3. extern "C" template<typename T>
  4. void invalid(); // ERROR: templates cannot have C linkage
  5. extern "Java" template<typename T>
  6. void javaLink(); // nonstandard, but maybe some compiler will someday
  7. // support linkage compatible with Java generics

模板通常有外部链接。唯一的一些例外是命名空间作用域中具有静态限定符的函数模板、匿名空间的直接或间接的成员的模板(它们拥有内部链接)以及匿名类的成员模板(它们没有链接)。

举个例子:

  1. template<typename T> // refers to the same entity as a declaration of the
  2. void external(); // same name (and scope) in another file
  3. template<typename T> // unrelated to a template with the same name in
  4. static void internal(); // another file
  5. template<typename T> // redeclaration of the previous declaration
  6. static void internal();
  7. namespace {
  8. template<typename> // also unrelated to a template with the same name
  9. void otherInternal(); // in another file, even one that similarly appears
  10. } // in an unnamed namespace
  11. namespace {
  12. template<typename> // redeclaration of the previous template declaration
  13. void otherInternal();
  14. }
  15. struct {
  16. template<typename T> void f(T) {} // no linkage: cannot be redeclared
  17. } x;

注意到最后面的成员模板没有链接,它必须在匿名类定义处定义,因为想要在类外部定义是不可能的。

当前,模板无法在函数作用域或局部类作用域中声明,但是泛化的lambda可以(参考P309节15.10.6),它有一个关联的闭包类型,其中包含了成员函数模板,其可以在局部作用域中出现,这实际上意味着一种局部成员函数模板。

模板实例的链接就是模板的链接。例如,函数internal<void>()从上面声明的模板internal实例化出来,它会拥有一个内部链接。而对于变量模板来说,这会产生一个有趣的后果。实际上,考虑下例:

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

zero所有实例化的实例都拥有一个外部链接,即使哪怕形如zero<int const>也是如此。这可能对既定的拥有一个内部链接的int const zero_int = int{};来说是违反直觉的,毕竟它使用了一个const类型来做修饰。同样的,模板template<typename T> int const max_volume = 11;实例化的所有实例也都拥有外部链接,尽管那些实例同样都是类型int const

12.1.3 主模板

模板的一般性声明声明了主模板(primary templates)。如此声明的模板在模板名后无需书写尖括号模板参数子句。

  1. template<typename T> class Box; // OK: primary template
  2. template<typename T> class Box<T>; // ERROR: does not specialize
  3. template<typename T> void translate(T); // OK: primary template
  4. template<typename T> void translate<T>(T); // ERROR: not allowed for functions
  5. template<typename T> constexpr T zero = T{}; // OK: primary template
  6. template<typename T> constexpr T zero<T> = T{}; // ERROR: does not specialize

非主模板会在声明类模板或变量模板的偏特化时出现。这些将在第16章讨论。函数模板始终必须是主模板(参考P356节17.3,这里讨论了未来语言变化的某种潜在可能)。

12.2 模板参数(Template Parameters)

有三种基本类型的模板参数:

  1. 类型参数(目前最常用的)
  2. 非类型模板参数
  3. 模板模板参数

这些基本类型的模板参数中的任何一种都可以用作模板参数包的基础(参考P188节12.2.4)。

模板参数在模板声明的参数化引导子句中声明,该声明无需命名:

  1. template<typename, int>
  2. class X; // X<> is parameterized by a type and an integer

当然,参数是否需要名称取决于模板后面的语句。还要注意,模板参数名可以在后续参数声明中引用(但前置则不行):

  1. template<typename T, //the first parameter is used
  2. T root, // in the declaration of the second one and
  3. template<T> class Buf> // in the declaration of the third one
  4. class Structure;

12.2.1 类型参数

类型参数由关键字typenameclass所引导:二者是完全等价的。关键字后必须有一个简单的标识符,并且该标识符后必须带有逗号,以表示下一个参数声明的开始,闭合的尖括号>用以指示参数化子句的结束,=用以指示一个默认模板参数的起始。

在模板声明内,类型参数的行为与类型别名(type alias)非常相似(参考P38节2.8)。例如,当T是模板参数时,即使T是被某种类(class)类型替换,也不能使用形如class T的详尽名称:

  1. template<typename Allocator>
  2. class List {
  3. class Allocator* allocptr; // ERROR: use "Allocator* allocptr"
  4. friend class Allocator; // ERROR: use "friend Allocator"
  5. ...
  6. };

12.2.2 非类型参数

非类型模板参数表示一个可以在编译期或链接期确定的常量值。这样的参数类型(换句话说,它所代表的值类型)必须是以下之一:

  • 整型或枚举型
  • 指针类型
  • 成员指针类型
  • 左值引用类型(既可以是对象引用,也可以是函数引用)
  • std::nullptr_t
  • 包含autodecltype(auto)的类型(C++17后支持;可参考P296节15.10.1)

其他类型当前都不支持(尽管浮点数在未来会被支持;可参考P356节17.2)。

也许令人惊讶的是,在某些情况下,非类型模板参数的声明也可以以关键字typename开头:

  1. template<typename T, // a type parameter
  2. typename T::Allocator* Allocator> // a nontype parameter
  3. class List;
  4. template<class X*> // a nontype parameter of pointer type
  5. class Y;

这两种情形很容易辨别,因为第一种的后面跟随了一个简单的标识符,然后是一小段标记(’=’用以表示默认参数,’,’用以指示后面的另一个模板参数,’>’用以闭合模板参数列表)。P67节5.1和P229节13.3.2对第一个非类型模板参数的关键字typename做出了解释(译者注:这里的typename是用来表示AllocatorT内的一个类型,而非静态数据成员)。

函数和数组类型可以被指定,但是它们会通过退化(decay)隐式地调整为相应的指针类型:

  1. template<int buf[5]> class Lexer; // buf is really an int*
  2. template<int* buf> class Lexer; // OK: this is a redeclaration
  3. template<int fun()> struct FuncWrap; // fun really has pointer to
  4. // function type
  5. template<int (*)()> struct FuncWrap; // OK: this is a redeclaration

非类型模板参数的声明与变量声明非常相似,但是它们不可以有非类型指示符,比如staticmutable等等。它们可以有constvolatile限定符,但是如果这种限定符出现在参数类型的最顶层,就会被忽略(译者注:换句话说,对左值引用或指针来说支持底层const):

  1. template<int const length> class Buffer; // const is useless here
  2. template<int length> class Buffer; // same as previous declaration

最后,在表达式中使用时,非引用类型的非类型参数始终都是prvalues(译者注:pure right values,即纯右值)。它们的地址无法被窃取,也无法被赋值。而另一方面,左值引用类型的非类型参数是可以像左值一样使用的:

  1. template<int& Counter>
  2. struct LocalIncrement {
  3. LocalIncrement() { Counter = Counter + 1; } // OK: reference to an integer
  4. ~LocalIncrement() { Counter = Counter - 1; }
  5. };

右值引用是不被允许的。

12.2.3 模板模板参数

模板模板参数是类或别名模板的占位符。它们的声明与类模板很像,但是不能使用关键字structunion

  1. template<template<typename X> class C> // OK
  2. void f(C<int>* p);
  3. template<template<typename X> struct C> // ERROR: struct not valid here
  4. void f(C<int>* p);
  5. template<template<typename X> union C> // ERROR: union not valid here
  6. void f(C<int>* p);

C++17允许用typename替代class,如是改动是受这一情况驱使的:模板模板参数不仅可以由类模板替代,而且可以由别名模板(可以实例化为任意类型)替代。因此,在C++17中,我们的上例可以改写成如下形式:

  1. template<template<typename X> typename C> // OK since C++17
  2. void f(C<int>* p);

在其声明的作用域内,模板模板参数用起来就像另一个类模板或是别名模板一样。

模板模板参数的参数可以有默认模板参数。在使用模板模板参数而未指定相应的参数时,这些默认参数会生效:

  1. template<template<typename T,
  2. typename A = MyAllocator> class Container>
  3. class Adaptation {
  4. Container<int> storage; // implicitly equivalent to Container<int,MyAllocator>
  5. ...
  6. };

TA都是模板模板参数Container的模板参数名称。这些名称仅在该模板模板参数的其他参数的声明中使用。以下设计模板说明了此概念:

  1. template<template<typename T, T*> class Buf> // OK
  2. class Lexer {
  3. static T* storage; // ERROR: a template template parameter cannot be used here
  4. ...
  5. };

但是,通常在其他模板参数的声明中不需要模板模板参数的模板参数名称,因此常常根本不命名。例如,我们早期的Adaptation模板可以按如下声明:

  1. template<template<typename,
  2. typename = MyAllocator> class Container>
  3. class Adaptation {
  4. Container<int> storage; // implicitly equivalent to Container<int,MyAllocator>
  5. ...
  6. };

12.2.4 模板参数包

从C ++ 11开始,可以通过在模板参数名称之前引入省略号(…)来将任何类型的模板参数转换为模板参数包(如果模板参数匿名,那么就在模板参数名称本该出现的位置之前):

  1. template<typename... Types> // declares a template parameter pack named Types
  2. class Tuple;

模板参数包的行为与其基础模板参数类似,但有一个关键的区别:普通的模板参数严格匹配某一个模板实参(template argument),而模板参数包可以匹配任意数量的模板实参。这意味着上面声明的Tuple类模板可以接受任意数量任意类型(很可能彼此不一样)的模板实参:

  1. using IntTuple = Tuple<int>; // OK: one template argument
  2. using IntCharTuple = Tuple<int, char>; // OK: two template arguments
  3. using IntTriple = Tuple<int, int, int>; // OK: three template arguments
  4. using EmptyTuple = Tuple<>; // OK: zero templates arguments

同样,非类型参数和模板模板参数的模板参数包可以分别接受任意数量的非类型或模板模板实参,分别为:

  1. template<typename T, unsigned... Dimensions>
  2. class MultiArray; // OK: declares a nontype template parameter pack
  3. using TransformMatrix = MultiArray<double, 3, 3>; // OK: 3x3 matrix
  4. template<typename T, template<typename,typename>... Containers>
  5. void testContainers(); // OK: declares a template template parameter pack

MultiArray示例需要全部的非类型模板实参均为相同的unsigned类型。C++17引入了非类型模板实参的推导,这将允许我们解除这一限制而做一些扩展(参考P298节15.10.1了解更多细节)。

主模板中的类模板、变量模板和别名模板至多只可以有一个模板参数包,且模板参数包必须作为最后一个模板参数。函数模板则少些限制:允许多个模板参数包,只要模板参数包后面的每个模板参数都具有默认值(请参阅下一节)或可以推导(参考第15章):

  1. template<typename... Types, typename Last>
  2. class LastType; // ERROR: template parameter pack is not the last template parameter
  3. template<typename... TestTypes, typename T>
  4. void runTests(T value); // OK: template parameter pack is followed
  5. // by a deducible template parameter
  6. template<unsigned...> struct Tensor;
  7. template<unsigned... Dims1, unsigned... Dims2>
  8. auto compose(Tensor<Dims1...>, Tensor<Dims2...>); // OK: the tensor dimensions can be deduced

最后一个例子使用了返回类型推导——C++14的特性。可以参考P296节15.10.1。

类和变量模板的偏特化声明(参考第16章)可以有多个参数包,这与主模板不同。这是因为偏特化是通过与函数模板几乎相同的推导过程所选择的。

  1. template<typename...> Typelist;
  2. template<typename X, typename Y> struct Zip;
  3. template<typename... Xs, typename... Ys>
  4. struct Zip<Typelist<Xs...>, Typelist<Ys...>>;
  5. // OK: partial specialization uses deduction to determine
  6. // the Xs and Ys substitutions

也许不足为奇的是,类型参数包不能在其自己的参数子句中进行扩展。例如:

  1. template<typename... Ts, Ts... vals> struct StaticValues {};
  2. // ERROR: Ts cannot be expanded in its own parameter list

然而,嵌套模板可以创造相似的有效情景:

  1. template<typename... Ts> struct ArgList {
  2. template<Ts... vals> struct Vals {};
  3. };
  4. ArgList<int, char, char>::Vals<3, 'x', 'y'> tada;

包含模板参数包的模板被称为可变参数模板(variadic template),因为它接受可变数量的模板参数。第4章和P200节12.4介绍了可变参数模板的使用。

12.2.5 默认模板实参

非模板参数包的任何类别的模板参数都可以配置默认参数,尽管它必须与相应的参数匹配(例如,类型参数不能有一个非类型默认实参)。默认实参不能依赖于其自身的参数,因为参数的名称直到默认实参之后才在作用域内生效。然而,他可以依赖前面的参数:

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

当且仅当还为后续参数提供了默认参数时,类模板、变量模板或别名模板的模板参数才可以具有默认模板实参。(对默认函数调用参数来说有着相似的限制条件。)通常在同一模板声明中提供后续所有的默认值,但也可以在该模板的先前声明中声明它们。下面的例子可以清楚地做出解释:

  1. template<typename T1, typename T2, typename T3,
  2. typename T4 = char, typename T5 = char>
  3. class Quintuple; // OK
  4. template<typename T1, typename T2, typename T3 = char,
  5. typename T4, typename T5>
  6. class Quintuple; // OK: T4 and T5 already have defaults
  7. template<typename T1 = char, typename T2, typename T3,
  8. typename T4, typename T5>
  9. class Quintuple; // ERROR: T1 cannot have a default argument
  10. // because T2 doesn't have a default

函数模板的模板参数的默认模板实参不需要后续的模板参数必须有一个默认模板实参:

  1. template<typename R = void, typename T>
  2. R* addressof(T& value); // OK: if not explicitly specified, R will be void

默认模板实参不允许重复声明:

  1. template<typename T = void>
  2. class Value;
  3. template<typename T = void>
  4. class Value; // ERROR: repeated default argument

许多上下文不允许使用默认模板实参:

  • 偏特化:
    1. template<typename T>
    2. class C;
    3. ...
    4. template<typename T = int>
    5. class C<T*>; // ERROR
  • 参数包:
    1. template<typename... Ts = int> struct X; // ERROR
  • 类模板成员类外定义: ```cpp template struct x { T f(); };

template // ERROR T X::f() {
… }

  1. - 友元类模板声明:
  2. ```cpp
  3. struct S {
  4. template<typename = void> friend struct F;
  5. };
  • 友元函数模板声明,除非它是一个定义并且它在编译单元的其他任何地方都没有声明: ```cpp struct S{ template friend void f(); // ERROR: not a definition template friend void g() { // OK so far } };

template void g(); // ERROR: g() was given a default template argument // when defined; no other declaration may exist here

  1. ## 12.3 模板实参(Template Arguments)
  2. 实例化模板时,模板实参会替换模板参数。模板实参可以被各种不同类型的机制所判定:
  3. - 显式模板实参:模板名称后可以跟随在尖括号内显式指定的模板实参。这种名称被叫做模板IDtemplate-id)。
  4. - 注入式类名:在具有模板参数`P1,P2 ...`的类模板`X`的作用域内,该模板(`X`)的名称可以等价于模板ID `X<P1, P2, ...>`。可以参考P22113.2.3了解更多细节。
  5. - 默认模板实参:如果默认模板实参可用,则可以在模板实例化时省略显式的模板实参。然而,对于类模板或别名模板来说,即使模板参数有默认值,尖括号也不能省略(其内可以为空)。
  6. - 实参推导:没有被显式指定的函数模板参数会通过函数调用的实参类型来进行推导。在第15章对细节进行了描述。在一些其他情景中也会完成推导。如果所有的模板实参都可以被推导,那么函数模板的名称后就无需书写尖括号子句。C++17还引入了从变量声明或函数符号类型转换的初始化器中推导类模板实参的能力。可以参考P31315.12中对此的一个探讨。
  7. ### 12.3.1 函数模板实参
  8. 函数模板的模板实参可以被显式地指定,它会从模板被使用的方式来做推导,或是由默认的模板实参来提供。例如:
  9. *details/max.cpp*
  10. ```cpp
  11. template<typename T>
  12. T max(T a, T b)
  13. {
  14. return b < a ? a : b;
  15. }
  16. int main()
  17. {
  18. ::max<double>(1.0, -3.0); // explicitly specify template argument
  19. ::max<1.0, -3.0); // template argument is implicitly deduce to be double
  20. ::max<int>(1.0, 3.0); // the explicit <int> inhibits the deduction;
  21. // hence the result has type int
  22. }

某些模板实参永远不会被推导,这可能是因为它们所对应的模板参数并没有在函数参数类型中出现或是一些其他原因(参考P271节15.2)。相应的参数通常放置在模板参数列表的开头,因此可以显式地指定它们,同时也允许推导其他参数。例如:

details/implicit.cpp

  1. template<typename DstT, typename SrcT>
  2. DstT implicit_cast (SrcT const& x) // SrcT can be deduced, but DstT cannot
  3. {
  4. return x;
  5. }
  6. int main()
  7. {
  8. double value = implicit_cast<double>(-1);
  9. }

如果我们翻转示例中模板参数的顺序(换句话说,写成template<typename SrcT, typename DstT>),implicit_cast的调用就必须同时显式地指定两个参数。

此外,这样的参数不能合法地放在模板参数包之后或在偏特化中出现,因为无法明确地指定或推导它们。

  1. template<typename... Ts, int N>
  2. void f(double (&)[N+1], Ts... ps); // useless declaration because N
  3. // cannot be specified or deduced

由于函数模板可以重载,为函数模板显式地指定所有的实参可能也无法充分指定某一个特定函数:在某些场景中,它选中了一个函数集。下面的例子阐述了这一现象:

  1. template<typename Func, typename T>
  2. void apply(Func funcPtr, T x)
  3. {
  4. funcPtr(x);
  5. }
  6. template<typename T> void single(T);
  7. template<typename T> void multi(T);
  8. template<typename T> void multi(T*);
  9. int main()
  10. {
  11. apply(&single<int>, 3); // OK
  12. apply(&multi<int>, 7); // ERROR: no single multi<int>
  13. }

本例中,第一个apply()调用可以成功是因为表达式&single<int>没有歧义。如此,模板实参值Func就可以被轻易的推断。在第二个调用中,&multi<int>可能是2种不同的类型,因此Func无法被推导。

更进一步,在函数模板中替换模板实参可能会导致尝试构造无效的C++类型或表达式。考虑下面的重载函数模板(RT1RT2没有指定类型):

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

表达式test<int>对于上述两种函数模板的前者来说都是没有意义的,因为类型int并没有成员类型X。然而,后者没有这样的问题。因此,表达式&test<int>标志了一个特定函数的地址。将int替换第一个函数模板失败的事实并不会使表达式无效。这一SFINAE (substitution failure is not an error)原则对函数模板的重载来说是非常关键的一部分,我们会在P129节8.4和P284节节15.7中讨论。

12.3.2 类型实参

模板类型实参是模板类型参数的选定“值”。任何类型(包括void,函数类型,引用类型等等)通常来说都可以作为模板实参,但是它们对模板参数的替换构成必须是合法的:

  1. template<typename T>
  2. void clear(T p)
  3. {
  4. *p = 0; // requires that the unary * be applicable to T
  5. }
  6. int main()
  7. {
  8. int a;
  9. clear(a); // ERROR: int doesn't support the unary *
  10. }

12.3.3 非类型实参

非类型实参是指那些替换非类型模板参数的值。这种值必须是以下其中一项:

  • 另一个具有正确类型的非类型模板参数。
  • 整型(或枚举)类型的编译器常量。只有在相应的参数具有一个匹配该类型或是一个无需缩小(narrowing)而可以被隐式转换到该类型的值的时候才可以接受。例如,char值可以提供给int参数,但是500对于char这一8位参数来说却是无效的。
  • 外部变量或函数的名称,其前面带有内置的一元(“取址”)运算符。对于函数和数组变量,可以省略。此类模板实参与指针类型的非类型参数匹配。 C++17放宽了此要求,允许任何的常量表达式产生一个指向函数或变量的指针。
  • 对于引用类型的非类型参数,前一种(但不带运算符)实参是有效实参。同样地,C++17在这里也放宽了约束,允许任意的常量表达式glvalue应用于函数或变量。
  • 成员指针常量;换句话说,表达式形如&C::m,其中C是类类型,m是非静态成员(数据或函数)。这只会匹配成员指针类型的非类型参数。同样的,在C++17中,实际的语法形式不再受限制:对匹配的成员指针常量的任何常量表达式求值都会被允许。
  • 空指针常量对指针或成员指针的非类型参数来说都是合法的。

对整型类型的非类型参数来说(可能也是最常用的非类型参数),到这一参数类型的隐式转换是可行的。随着C++ 11中constexpr转换函数的引入,这意味着转换前的参数可以具有类类型。

C++17之前,将实参与作为指针或引用的参数进行匹配时,不会考虑用户定义的转换(单参数构造函数和转换运算符)和派生类到基类的转换,即使在其他情况下它们是有效的隐式转换。使得实参更const和/或更volatile的隐式转换是可行的。

下面是一些有效的非类型模板实参的例子:

  1. template<typename T, T nontypeParam>
  2. class C;
  3. C<int, 33>* c1; // integer type
  4. int a;
  5. C<int*, &a>* c2; // address of an external variable
  6. void f();
  7. void f(int);
  8. C<void (*)(int), f>* c3; // name of a function: overload resolution selects
  9. // f(int) in this case; the & is implied
  10. template<typename T> void templ_func();
  11. C<void(), &templ_func<double>>* c4; // function template instantiations are functions
  12. struct X {
  13. static bool b;
  14. int n;
  15. constexpr operator int() const { return 42; }
  16. };
  17. C<bool&, X::b>* c5; // static class members are acceptable variable/function names
  18. C<int X::*, &X::n>* c6; // an example of a pointer-to-member constant
  19. C<long, X{}>* c7; // OK: X is the first converted to int via a constexpr conversion
  20. // function and then to long via a standard integer conversion

模板实参的一个通用限制在于编译器或链接器必须在程序构建时有能力表示它们的值。在程序运行前无法知晓的值(例如,局部变量的地址)在程序构建时与模板实例化的概念是不相容的。

尽管如此,还是有着一些常量,可能有些吃惊,目前也是无效的:

  • 浮点数
  • 字符串字面量(C++11之前,空指针常量也不行)

字符串字面量的一个问题在于两个相同的字面量可以存储在不同的地址上。对常量字符串做模板实例化有另一种迂回的方法(但麻烦),这涉及了引入一个附加变量来保存字符串:

  1. template<char const *str>
  2. class Message {
  3. ...
  4. };
  5. extern char const hello[] = "Hello Wolrd!";
  6. char const hello11[] = "Hello World!";
  7. void foo()
  8. {
  9. static char const hello17[] = "Hello World!";
  10. Message<hello> msg03; // OK in all versions
  11. Message<hello11> msg11; // OK since C++11
  12. Message<hello17> msg17; // OK since C++17
  13. }

必要条件是声明为引用或指针的非类型模板参数必须是一个在C++全版本中拥有外部链接的常量表达式,自C++11起内部链接亦可,而C++17中则只要有任意某个链接即可。

参考P354节17.2对这一领域未来可能发生变化的一个讨论。

这里有些(少得可怜)合法的示例:

  1. template<typename T, T nontypeParam>
  2. class C;
  3. struct Base {
  4. int i;
  5. } base;
  6. struct Derived : public Base {
  7. } derived;
  8. C<Base*, &derived>* err1; // ERROR: derived-to-base conversions are not considered
  9. C<int&, base.i>* err2; // ERROR: fields of variables aren't considered to be variables
  10. int a[10];
  11. C<int*, &a[0]>* err3; // ERROR: addresses of array elements aren't acceptable either

12.3.4 模板模板实参

模板模板实参通常必须是一个严格匹配类模板或别名模板的模板参数的实参替换。C++17之前,模板模板实参的默认实参会被忽略(但是如果模板模板参数有默认实参,它们会在模板实例化时被考虑)。C++17放宽了这一匹配规则,它只需要模板模板参数至少被相应的模板模板实参特化(参考P330节16.2.2)。

在C++17之前下面的例子是非法的:

  1. #include <list>
  2. // declares in namespace std:
  3. // template<typename T, typename Allocator=allocator<T>>
  4. // class list;
  5. template<typename T1, typename T2, template<typename> class Cont> // Cont expects one parameter
  6. class Rel {
  7. ...
  8. };
  9. Rel<int, double, std::list> rel; // ERROR before C++17: std::list has more than
  10. // one template parameter

示例中的问题在于std::list这一标准库模板拥有多于一个的模板参数。第二个参数(描述一个allocator)拥有一个默认值,但是在C++17之前,在匹配std::listContainer参数时这并不会被考虑。

可变模板模板参数是C++17之前上述描述的“严格匹配”规则的一个例外,同时它也有一个解除这一限制的方案:它们对模板模板实参启用更通用的匹配。模板模板参数包可以匹配零到多个模板模板实参中的相同种类的模板参数。

译者注:这里相同种类不是指狭义的数据类型,而是指类型参数、非类型参数、函数模板参数、模板模板参数这些不同的类别(也就是12.3分开讨论的这些)。

  1. #include <list>
  2. template<typename T1, typename T2,
  3. template<typename... > class Cont> // Cont expects any number of
  4. class Rel { // type parameters
  5. ...
  6. };
  7. Rel<int, double, std::list> rel; // OK: std::list has two template parameters
  8. // but can be used with one argument

模板参数包只能匹配相同种类的模板参数。例如,下面的类模板可以使用仅有一个模板参数类型的任意类模板或别名模板实例化,因为模板类型参数包在这里传递的TT可以匹配零到多个模板类型参数:

  1. #include <list>
  2. #include <map>
  3. // declares in namespace std;
  4. // template<typename Key, typename T,
  5. typename Compare = less<Key>,
  6. typename Allocator = allocator<pair<Key const, T>>>
  7. // class map;
  8. #include <array>
  9. // declares in namespace std;
  10. // template<typename T, size_t N>
  11. // class array;
  12. template<template<typename... > class TT>
  13. class AlmostAnyTmpl {
  14. };
  15. AlmostAnyTmpl<std::vector> withVector; // two type parameters
  16. AlmostAnyTmpl<std::map> witMap; // four type parameters
  17. AlmostAnyTmpl<std::array> withArray; // ERROR: a template type parameter pack
  18. // doesn't match a nontype template parameter

在C++17之前,声明模板模板参数只能使用关键字class,但这并不代表仅允许将用关键字class声明的类模板用作替换参数。实际上,structunion以及别名模板也都是模板模板参数的合法实参(别名模板是C++11后才出现并支持)。这类似于这一现象:任何类型都可以用作关键字class声明的模板类型参数的实参。

12.3.5 等效性(equivalent)

当两组模板实参的每一对参数值都相同时,它们被称视为等效的。对于类型参数,类型别名无关紧要:最终比较的是类型别名所声明的底层类型。对于整型非类型实参,参数的值会被比较;这个值如何表示无关紧要。下面的例子阐述了这一概念:

  1. template<typename T, int I>
  2. class Mix;
  3. using Int = int;
  4. Mix<int, 3*3>* p1;
  5. Mix<int, 4+5>* p2; // p2 has the same type as p1

(正如这一示例所澄清,无需模板定义即可确定模板参数列表的等效性。)

在模板依赖上下文中,模板实参的”值“却是无法一直被明确确定的,且对等效性来说这里的规则更加复杂。考虑下例:

  1. template<int N> struct I {};
  2. template<int M, int N> void f(I<M+N>); // #1
  3. template<int N, int M> void f(I<N+M>); // #2
  4. template<int M, int N> void f(I<N+M>); // #3 ERROR

谨慎声明#1和#2,你将注意到它们仅仅是交换重命名了的MN,你得到了相同的声明:二者是等效的,它们声明了相同的模板f。表达式M+NN+M在这两个声明中被视为等效的。

然而#3的声明,确是有着巧妙的不同:只有操作数被翻转。这会让表达式N+M与前两者都不等效。但是,由于该表达式将对所涉及的模板参数的任何值产生相同的结果,因此这些表达式被称为功能等效的(functionally equivalent)。模板以这种不同的方式声明是错误的,仅因为声明功能等效的表达式实际上并不等效。

然而,编译器无需诊断此类错误。这是因为某些编译器可能,举例来说,在内部将N+1+1表示为等同的N+2,但其他编译器则不然。该标准没有强加一种特定的实现方法,而是二者皆允并要求程序员在此方面保持谨慎。

函数模板生成的函数与普通的函数永远不是等效的,尽管他们可能有相同的类型和名称。这对类成员来说产生了两个重要影响:

  1. 成员函数模板生成的函数永远不会覆盖(override)虚函数。
  2. 构造器模板生成的构造器永远不会是拷贝或移动构造器。类似的,赋值操作符模板生成的赋值操作符函数也永远不会是拷贝赋值或是移动赋值操作符函数。(然而,由于隐式调用拷贝赋值或移动赋值操作符函数的情景相对少,所以这一般不会引起问题。)这一事实各有优劣。可以参考P95节6.2和P102节6.4了解更多细节。

12.4 可变模板

在P55节4.1中介绍的可变模板参数,是指那些至少包含一个模板参数包(参考P188节12.2.4)的模板。当模板的行为可以泛化为任意数量实参时可变模板将非常有用。P188节12.2.4引入的Tuple类模板就是一个可变模板,因为一个tuple可以有任意数量的元素,它们被同等对待。我们也可以想象一个简单的print()函数,它携带任意数量的参数并按顺序打印每一个。

当可变模板的模板实参被确定时,可变模板的每个模板参数包都将匹配连续的零到多个模板实参。我们将此模板实参序列称为实参包(argument pack)。下面的例子阐述了模板参数包Types是如何根据Tuple所提供的模板实参而匹配不同的实参包的。

  1. template<typename... Types>
  2. class Tuple {
  3. // provides operations on the list of types in Types
  4. };
  5. int main() {
  6. Tuple<> t0; // Types contains an empty list
  7. Tuple<int> t1; // Types contains int
  8. Tuple<int, float> t2; // Types contains int and float
  9. }

由于模板参数包代表了若干个而不是单一的模板实参,它必须在实参包中所有参数都被应用的相同语法结构上下文中使用。这样的结构之一就是sizeof...操作符,它会对实参包中实参的个数进行计数。

  1. template<typename... Types>
  2. class Tuple {
  3. public:
  4. static constexpr std::size_t length = sizeof...(Types);
  5. };
  6. int a1[Tuple<int>::length]; // array of the integer
  7. int a3[Tuple<short, int, long>::length]; // array of three integers

12.4.1 包展开(Pack Expansions)

sizeof...表达式是包展开的一个例子。包展开是一种把一个实参包展开成独立实参的结构。sizeof...执行这一展开只是为了去计数独立实参的个数,其他形式的实参包——那些在C++渴望一个列表的场合——可以将列表展开成多个元素。这样的包展开由列表中元素右侧的省略号(…)标识。这里有一个简单的例子,我们创建了一个新的类模板MyTuple,它传递其实参并从Tuple所继承:

  1. template<typename ...Types>
  2. class MyTuple : public Tuple<Types..> {
  3. // extra operations provided only for MyTuple
  4. };
  5. MyTuple<int, float> t2; // inherits from Tuple<int, float>

模板实参Types...是一个包展开,它产生了一个模板实参序列,实参包中的每个实参都用于取代Types。如例子中所展示,实例化的类型MyTuple<int, float>的模板类型参数包types被实参包int, float所取代。当出现在参数展开Types...时,我们得到一个模板实参int和另一个模板实参float,因此MyTuple<int, float>Tuple<int, float>所继承。

理解包展开的一种直观方法是根据语法展开来思考它们,模板参数包将被正确数量的(非包)模板参数替换,并且包展开被写为单独的参数,每个非包类型的模板参数各一个。例如,MyTuple被展开成两个参数应该长这个样子:

  1. template<typename T1, typename T2>
  2. class MyTuple : public Tuple<T1, T2> {
  3. // extra operations provided only for MyTuple
  4. };

三个参数则长这样子:

  1. template<typename T1, typename T2, typename T3>
  2. class MyTuple : public Tuple<T1, T2, T3> {
  3. // extra operations provided only for MyTuple
  4. };

然而请注意,你无法直接通过名字来访问参数包中的独立元素,因为T1, T2等名字并没有在可变模板中定义。如果你需要类型,唯一可以做的事就是传递它们(非递归地)给另一个类或函数。

每个包展开都有一个模式(pattern),它是一个被实参包的每个实参所替换的类型或表达式,并且通常出现在表示包展开的省略号之前。我们前面的例子都只有些无关紧要的模式——参数包的名称——但是模式可以更为复杂。例如,我们可以定义一个新类型PtrTuple,它继承于实参类型的指针所构成的Tuple

  1. template<typename... Types>
  2. class PtrTuple : public Tuple<Types*...> {
  3. // extra operations provided only for PtrTuple
  4. };
  5. PtrTuple<int, float> t3; // Inherits from Tuple<int*, float*>

包展开Types*...的模式是Types*。该模式产生了一个模板类型实参替换的序列,每个实参的类型都被其对应的指针类型所取代,并应用于Types中。在包展开的语法解释下,这是如果将PtrTuple扩展为三个参数时看起来的样子:

  1. template<typename T1, typename T2, typename T3>
  2. class PtrTuple : public Tuple<T1*, T2*, T3*> {
  3. // extra operations provided only for PtrTuple
  4. };

12.4.2 包展开可以在哪里出现?

我们目前的例子都是聚焦于使用包展开来产生一个模板实参序列。实际上,包展开基本上可以在语法提供逗号分隔列表的任何位置使用,这包括:

  • 基类列表
  • 构造器中的基类初始化列表(initializer)
  • 调用实参列表(模式就是实参表达式)
  • 初始化列表(例如,在花括号初始化列表(initializer list))
  • 类、函数或别名模板的模板参数列表
  • 函数可以抛出的异常列表(自C++11起不建议使用、C++17后不再允许)
  • 在属性内,如果属性本身支持包展开(尽管在C++标准中没有定义这样的属性)
  • 指定某个声明的对齐方式时
  • 指定lambda表达式捕获列表时
  • 函数类型的参数列表
  • using声明中(自C++17起支持;参考P65节4.4.5)。我们已经提到过sizeof...作为一种包展开机制,它并不会真正产生一个列表,C++17也增加了表达式折叠(fold expressions),这是另一种不产生逗号分隔的列表的机制(参考P207节12.4.6)

上述包展开所在的某些上下文只是为了归纳的完整性,因此,我们仅将注意力集中在那些在实践中往往有用的包展开上下文上。毕竟包展开在所有上下文中都遵循相同的原则和语法,你大可从此处给出的示例推断出是否需要更深奥的包展开上下文。

在基类列表中的包展开会扩展成多个直接基类。这种扩展对于通过mixins聚合外部提供的数据和功能很有用,mixins是旨在“混合到”类层次结构中以提供新行为的类。例如,下面的Point类在多个不同上下文中使用了包展开以允许任意的mixins:

  1. template<typename... Mixins>
  2. class Point : public Mixins... { // base class pack expansion
  3. double x, y, z;
  4. public:
  5. Point() : Mixins()... { } // base class initializer pack expansion
  6. template<typename Visitor>
  7. void visitMixins(Visitor visitor) {
  8. visitor(static_cast<Mixins&>(*this)...); // call argument pack expansion
  9. }
  10. };
  11. struct Color { char red, green, blue; };
  12. struct Label { std::string name; };
  13. Point<Color, Label> p; // inherits from both Color and Label

Point类使用包扩展来获取每个提供的mixin,并将其扩展为公有继承的基类。Point的默认构造器在类初始化列表中使用了包展开,对mixin机制引入的每个基类进行了值初始化。

成员函数模板visitMixins最有趣,它使用了包展开的结果作为调用参数。通过转换*this为每一种mixin类型,包展开生成了每个基类对应mixin类型的调用参数。P204节12.4.3中介绍了实际上与visitMixins一起使用而编写的visitor,它可以使用任意数量的函数调用参数。

包展开也在模板参数列表中创建非类型模板参数包时使用:

  1. template<typename... Ts>
  2. struct Values {
  3. template<Ts... Vs>
  4. struct Holder {
  5. };
  6. };
  7. int i;
  8. Values<char, int, int*>::Holder<'a', 17, &i> valueHolder;

注意一旦Values<..>的类型实参被确定,Values<...>::Holder的非类型实参列表就是固定的尺寸;参数包Vs就不是一个变长参数包。

Values是一个非类型模板参数包,其中每个真实的模板实参都可以是不同的类型,它们由模板类型参数包Types提供的类型所指定。请注意,Values声明中的省略号起着双重作用,既将模板参数声明为模板参数包,又将该模板参数包的类型声明为一个包展开。这种模板参数包在实践中非常罕见,而在一个更加常见的上下文——函数参数中这种规则同样生效。

12.4.3 函数参数包

函数参数包(function parameter pack)是一个匹配零到多个函数调用实参的函数参数。与模板参数包相似,函数参数包通过在函数参数名前使用前置省略号引入,同样地,函数参数包在使用时必须由包展开来扩展。模板参数包和函数参数包被统一称作参数包(parameter packs)。

与模板参数包不同的是,函数参数包始终都是包展开,因此它们声明的类型必须包含至少一个参数包。下面的例子中,我们引入一个新的Point构造器,使用提供的构造器实参来拷贝初始化每一个mixin:

  1. template<typename... Mixins>
  2. class Point : public Mixins...
  3. {
  4. double x, y, z;
  5. public:
  6. // default constructor, visitor function, etc. elided
  7. Point(Mixins... mixin) // mixins is a function parameter pack
  8. : Mixins(mixins)...{ } // initialize each base with the supplied mixin value
  9. };
  10. struct Color { char red, green, blue; };
  11. struct Label { std::string name; };
  12. Point<Color, Label> p({0x7F, 0, 0x7F}, {"center"});

函数模板的函数参数包可能依赖于模板中声明的模板参数包,这使得函数模板可以接受任意数量的调用实参而不会损失类型信息:

  1. template<typename... Types>
  2. void print(Types... values);
  3. int main
  4. {
  5. std::string welcome("Welcome to ");
  6. print(welcome, "C++", 2011, '\n'); // calls print<std::string, char const*,
  7. // int, char>
  8. }

当使用多个实参调用函数模板print()时,实参的类型将放置在参数包中,以取代模板类型参数包Types,而实参本身则放入参数包中,以代替函数参数包Values。调用实参被确定的过程在第15章对细节进行了描述。当前,只要了解Types中的第i个类型对应Values的第i个值即可,并且这些参数包的每一对在函数模板print()内都是可用的。

print()的真正实现使用了递归的模板实例化,这是一种模板元编程技术,在P123节8.1和第23章中有所描述。

在参数列表末尾出现的匿名函数参数包与C样式的“ vararg”参数之间在语法上存在歧义。例如:

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

前者的T被视为T, ...:一个匿名参数类型T跟着一个C风格的vararg参数。后者的T...结构被视为一个函数参数包,因为T是一个合法的展开模式。可以通过在省略号前强制添加一个逗号(这保证了省略号被认作C风格vararg参数)或在省略号后跟随一个标识符——这意味着它是一个命名函数参数包来消除歧义。请注意,在通用的lambda中,如果紧随其后的类型(没有中间逗号)包含auto,则尾随的将被视为表示参数包。

12.4.4 多重与嵌套包展开

包展开的模式可以随意复杂且可以包含多重、不同的参数包。当实例化包含多重参数包的包展开时,所有的参数包都必须有相同的尺寸。从每个参数包的第一个实参开始进行模式替换,然后是每个参数包的第二个实参,以此类推,最终组织成类型或值的序列。例如,下面的函数在转发所有实参给函数对象f之前,对他们进行了拷贝:

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

调用实参包展开命名了两个实参包,Typesvalues。当实例化该模板时,Typesvalues参数包的逐个元素会产生一系列对象构造体,它们使用Types的第i个类型创建了values的第i个值。在包展开的语法解析下,三个实参的forwardCopy可能长这个样子:

  1. template<typename F, typename T1, typename T2, typename T3>
  2. void forwardCopy(F f, T1 const& v1, T2 const& v2, T3 const& v3) {
  3. f(T1(v1), T2(v2), T3(v3));
  4. }

包展开本身也可以嵌套。此时,每个参数包都可以由最近的一个闭合的包展开所扩展(也只能是这个包展开)。下面的例子阐释了引入3个不同参数包的嵌套包展开:

  1. template<typename... OuterTypes>
  2. class Nested {
  3. template<typename... InnerTypes>
  4. void f(InnerTypes const&... innerValues) {
  5. g(OuterTypes(InnerTypes(innerValues)...)...);
  6. }
  7. };

g()的调用中,模式InnerTypes(innerValues)的包展开是最内层的,它扩展了InnerTypesinnerValues并为OuterTypes表示的对象产生了一个函数调用实参序列。外层的包展开模式包含内层包展开,为函数g()产生了一个调用参数集,它们由内层包展开生成的函数调用实参序列所形成的OuterTypes中的每一种实例化类型所创造。在这种包展开的语法解析下,当OuterTypes有2个实参,InnerTypesinnerValues都有3个实参时,嵌套会变得更加明显:

  1. template<typename O1, typename O2>
  2. class Nested {
  3. template<typename I1, typename I2, typename I3>
  4. void f(I1 const& iv1, I2 const& iv2, I3 const& iv3) {
  5. g(O1(I1(iv1), I2(iv2), I3(iv3)),
  6. O2(I1(iv1), I2(iv2), I3(iv3)));
  7. }
  8. };

这里作者多写了一行O3

多重与嵌套包展开是一个非常强力的工具(例如,参考P608节26.2)。

12.4.5 零尺寸包展开

包展开的语法解析对于理解不同实参数量的可变模板实例化的方式非常有用。然而,对于零尺寸实参包来说语法解析经常会失败。为了说明这一点,请考虑P202节12.4.2中的Point类模板,该模板在语法上用零个实参替换:

  1. template<>
  2. class Point : {
  3. Point() : { }
  4. };

上面编写的代码格式不正确,因为模板参数列表现在为空,并且空的基类和基类初始化器列表每个都有一个冒号。

包展开实际上是语义结构,任意尺寸实参包的替换并不会影响包展开(或其封闭的可变参数模板)的解析。当包扩展展开成一个空列表时,程序的表现(语义上)就好像该列表不曾存在。实例化Point <>最终没有基类,并且其默认构造函数没有基类初始化程序,但其格式正确。这一语法规则使得即使是零尺寸的包展开也可以被完美定义(但有所区别)。例如:

  1. template<typename T, typename... Types>
  2. void g(Types... values) {
  3. T v(values...);
  4. }

可变函数模板g()创造了一个值v,它使用传入的values一系列值来直接初始化。如果values是空的,那么v在语法上看起来就好像是一个函数声明T v()。然而,因为包展开的替换是一种语法且解析时不会产生影响其他类型的实体,v会通过零个实参进行初始化,也就是说,这依然还是值初始化。

12.4.6 折叠表达式

对一连串的值进行同一模式的递归处理被称做操作的折叠。例如,对序列x[1],x[2],...,x[n-1],x[n]进行函数fn右折叠会得到fn(x[1],fn(x[2], fn(...,fn(x[n-1],x[n])...)))。在探索一种新的语言特性时,C++委员会遇到了需要特殊处理的结构:应用于包展开的二元逻辑运算符(即&&||)。在没有额外的语法特性时,我们需要编写下面的代码来实现&&操作:

  1. bool and_all() { return ture; }
  2. template<typename T>
  3. bool and_all(T cond) { return cond; }
  4. template<typename T, typename... Ts>
  5. bool and_all(T cond, Ts... conds) {
  6. return cond && and_all(conds...);
  7. }

C++17引入了一种新的特性——折叠表达式(fold expressions)(参考P58节4.2)。它可以应用于除了.->[]以外的所有的二元操作符。

给定一个未展开表达式模式pack和一个非模式表达式value,C++17允许我们使用任意操作符op写出:

  1. (pack op ... op value)

作为一个操作符右折叠(称作二元右折叠),或者写出:

  1. (value op ... op pack)

作为一个操作符左折叠(称作二元左折叠)。参考P58节4.2了解更多基本示例。

折叠操作应用于一个序列,对包进行展开并从最后一个(右折叠)或第一个(左折叠)序列中的元素施加value

有了这一特性,如下代码:

  1. template<typename... T> bool g() {
  2. return and_all(trait<T>()...);
  3. }

and_all在上面代码中定义),就可以被替换写成:

  1. template<typename... T> bool g() {
  2. return (trait<T>() && ... && true);
  3. }

如你所愿,折叠表达式是包展开。注意即使包为空,折叠表达式的类型仍然可以借由非包操作数(上例中是value)来确定。

然而,这一特性的设计者还希望增加一个摆脱value操作数的选项。在C++17中还支持另外两种形式:一元右折叠(pack op ...)和一元左折叠(... op pack)。

此时小括号依然是必须的。很明显对于空展开来说这产生了一个问题:如何确定它们的类型或是值呢?答案就是对于一元折叠表达式来说,空展开通常来说会导致一个错误,除了以下三种特例:

  • 单一折叠&&对空展开产生一个值true
  • 单一折叠||对空展开产生一个值false
  • 单一折叠,会产生表达式void

注意,如果你重载上述某个特殊的操作符时(通常不太常见),可能会出乎意料,例如:

  1. struct BooleanSymbol {
  2. ...
  3. };
  4. BooleanSymbol operator||(BooleanSymbol, BooleanSymbol);
  5. template<typename... BTs> void symbolic(BTs... ps) {
  6. BooleanSymbol result = (ps || ...);
  7. ...
  8. }

假设我们用从BooleanSymbol继承的类型来调用symbolic。对所有展开来说,除了空展开以外,都会产生一个BooleanSymbol值(空展开产生的是布尔值)。我们要注意一元折叠表达式的使用,并推荐以二元折叠表达式作为替代(显式地指定空展开值)。

12.5 友元

声明友元的初衷非常简单:在某个类中标记友元函数或友元类以使其获得访问特权。由于以下两个因素,事情变得有些复杂:

  1. 友元的声明必须是唯一的。
  2. 友元函数声明时可以直接定义。

12.5.1 类模板的友元类

友元类声明时不能定义,因此很少出问题。在模板的上下文中,友元类声明的唯一新奇之处是在于能够将类模板的特定实例声明为友元:

  1. template<typename T>
  2. class Node;
  3. template<typename T>
  4. class Tree {
  5. friend class Node<T>;
  6. ...
  7. };

请注意,类模板必须在其实例之一成为类或类模板的友元时是可见的。对普通类来说,则没有这种要求:

  1. template<typename T>
  2. class Tree {
  3. friend class Factory; // OK even if first declaration of Factory
  4. friend class Node<T>; // error if Node isn't visible
  5. };

P220节13.2.2对此有更多描述。

P75节5.5引入了一个例子,给出了其他类模板实例做友元时的声明:

  1. template<typename T>
  2. class Stack {
  3. public:
  4. ...
  5. // assign stack of elements of type T2
  6. template<typename T2>
  7. Stack<T>& operator=(Stack<T2> const&);
  8. // to get access to private members of Stack<T2> for any type T2:
  9. template<typename> friend class Stack;
  10. ...
  11. };

C++11也增加了让模板参数作友元的语法:

  1. template<typename T>
  2. class Wrap {
  3. friend T;
  4. ...
  5. };

对任何类型T来说这都是合法的,如果T不是一个类类型的话,友元就会被忽略(译者注:基础类型不需要声明为友元)。

12.5.2 类模板的友元函数

函数模板的实例可以作为友元,只要保证友元函数名称后跟着一个尖括号子句即可。尖括号子句可以包含模板实参,但是如果实参可以被推导,那么尖括号就可以留空:

  1. template<typename T1, typename T2>
  2. void combine(T1, T2);
  3. class Mixer {
  4. friend void combine<>(int&, int&); // OK: T1 = int&, T2 = int&
  5. friend void combine<int, int>(int, int); // OK: T1 = int, T2 = int
  6. friend void combine<char>(char, int); // OK: T1 = char, T2 = int
  7. friend void combine<char>(char&, int); // ERROR: doesn't match combine() template
  8. friend void combine<>(long, long) { ... } // ERROR: definition not allowed!
  9. };

请注意,我们无法定义模板实例(最多可以定义一个特化体),因此友元声明不能是一个定义。

如果名称后没有跟尖括号子句,那么有两种可能:

  1. 如果名字没有限定符(换句话说,不包含::),它永远不会是一个模板实例。如果友元声明时不存在可见的匹配的非模板函数,此处的友元声明就作为该函数的第一次声明。该声明也可以是一个定义。
  2. 如果名称带有限定符(包含::),该名称必须可以引用到一个此前声明过的函数或函数模板。非模板函数会比函数模板优先匹配。然而,这里的友元声明不能是一个定义。这里有个例子来说明这一区别:
  1. void multiply(void*); // ordinary function
  2. template<typename T>
  3. void multiply(T); // function template
  4. class Comrades {
  5. friend void multiply(int) { } // defines a new function ::multiply(int)
  6. friend void ::multiply(void*); //refers to the ordinary function above,
  7. // not the the multiply<void*> instance
  8. friend void ::multiply(int); // refers to an instance of the template
  9. friend void ::multiply<double*>(double*); // qualified names can also have angle brackets,
  10. // but a template must be visible
  11. friend void ::error() { } // ERROR: a qualified friend cannot be a definition
  12. };

在前例中,我们在一个普通的类中声明了友元函数。在类模板中声明友元函数规则也是如此,只不过模板参数可以参与到函数声明中:

  1. template<typename T>
  2. class Node {
  3. Node<T>* allocate();
  4. ...
  5. };
  6. template<typename T>
  7. class List {
  8. friend Node<T>* Node<T>::allocate();
  9. };

函数模板也可以在类模板中定义,此时只有在它真正被使用到时才会实例化。通常,这要求友元函数以友元函数的类型使用类模板本身,这使得在类模板上表示函数变得更容易,就好像它们在命名空间中可见一样:

  1. template<typename T>
  2. class Creator {
  3. friend void feed(Creator<T>) { //every T instantiates a different function ::feed()
  4. ...
  5. }
  6. };
  7. int main()
  8. {
  9. Creator<void> one;
  10. feed(one); // instantiates ::feed(Creator<void>)
  11. Creator<double> two;
  12. feed(two); // instantiates ::feed(Creator<double>)
  13. }

示例中,Creator的每个实例都会生成一个不同的函数。请注意,即使这些函数是作为模板实例化的一部分生成的,这些函数本身也只是普通的函数,并不是模板的实例。然而,这种情况被视为模板实体(templated entities, 参考P181节12.1),它们仅在被使用到时才会被定义。同时也注意到由于这些函数的函数体在类定义域内被定义,所以它们是内联(inline)的。因此,两个不同编译单元生成该相同的函数并不会引起错误。可以参考P220节13.2.2和P497节21.2.1来了解该话题的更多信息。

12.5.3 友元模板

通常在声明一个函数或类模板的实例为友元时,我们可以严格地表示哪个实体才是友元。尽管如此,有些时候对某种模板的所有实例都设为友元也是很有用的。这就需要使用友元模板(friend template)。例如:

  1. class Manager {
  2. template<typename T>
  3. friend class Task;
  4. template<typename T>
  5. friend void Schedule<T>::dispatch(Task<T>*);
  6. template<typename T>
  7. friend int ticket() {
  8. return ++Manager::counter;
  9. }
  10. static int counter;
  11. };

与普通的友元声明一样,当名称是不含限定符的函数名时友元模板也可以是一个定义,函数名后不接尖括号子句。

友元模板只能定义主模板和主模板的成员。主模板的偏特化和显式特化也都会被自动的视作友元。

12.6 后记

C++模板的通用语法和概念自80年代起就相对保持稳定。类模板和函数模板是最开始时构成模板的两部分。类型模板和非类型模板也是。

然而,受C++标准库的需求所驱动,后来新增了一些重大的特性。成员模板可能是这些添加中最基础的。搞笑的是,只有成员函数模板被正式票入C++标准。成员类模板在社论监督下才成为标准的一部分。

友元模板,默认模板实参,模板模板参数是在C++98标准化后出现的。声明模板模板参数的能力有时被成为高阶泛型(higher-order genericity)。引入它们原本是为了支持一个已有的C++标准库的分配器(allocator)模型,然而这个分配器模型后来被另一个不依赖模板模板参数的模型取代了。后来,模板模板参数距离被踢出语言标准越来越近,因为它们的规范并不完整,直到非常晚才出现的1998标准化进程。最终大多数委员会成员投票表示保留它们,而它们的规范也完整制定。

别名模板是在2011标准引入的。别名模板为需要typedef templates特性而简化书写模板的场合提供了相同的服务,它仅仅是一个现有类模板的另一种拼写。规范(N2258)(作者是 Gabriel Dos Reis 和 Bjarne Stroustrup;)把它加入到标准。 Mat Marcus 也贡献了这一提议的一些早期草稿。Gaby 还为C++14(N3651)的可变模板提议处理了很多细节内容。本来,该提议仅仅想要支持constexpr变量,但是这一限制在标准制定阶段被解除了。

可变模板由C++11标准库和Boost库所驱动,C++模板库此前一直使用一种递进型高级技巧来支持接受任意数量模板参数。 Doug Gregor, Jaakko J¨arvi, Gary Powell, Jens Maurer, 和 Jason Merrill 为标准化提供了初始的规范(N2242)。当这一规范问世时,Doug 还开发了这一特性的原始实现代码(在GNU的GCC中),为标准库使用这一特性提供了极大助力。

折叠表达式是 Andrew Sutton 和 Richard Smith 的作品:它们通过N4191文献引入到C++17。