《C++Primer 第五版》——第十六章 模板与泛型编程

《C++Primer 第五版》——第十六章 模板与泛型编程


面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:OOP能处理类型在程序运行之前都不知道的情况;而在泛型编程中,在编译时就能获知类型了。
  
  前面介绍的容器、迭代器和算法都是泛型编程的例子。当我们编写一个泛型程序时,是独立与任何特定类型来编写代码的。当使用一个泛型程序时,我们提供类型或值,程序实例可在其上运行。
  
  模板是C++中泛型编程的基础。一个模板就是一个创建类或者函数的蓝图或者说公式。当使用一个vector这样的泛型类型,或者find这样的泛型函数时,由用户提供足够的信息来将蓝图转换为特定的类或函数,这种转换发生在编译时


16.1 定义模板

① 当程序员显式提供了模板实参
② 或对于函数模板(和类模板,C++17 起),当模板实参被推导出时
  它们替换对应的模板形参,以获得模板的一个 特化(specialization) ,即一个特定类型或一个特定函数左值。

16.1.1 函数模板

我们可以定义一个通用的 函数模板(function template) ,而不是为每个类型都定义一个新函数。一个函数模板就是一个公式,可用来生成针对特定类型的函数版本
  模板定义以关键字 template 开始,后跟一个 模板参数列表(template parameter list) ,这是一个逗号分隔的一个或多个 模板参数(template parameter) 的类表,用小于号<和大于号>包围起来。

例如:
  在下面代码中,我们的compare函数声明了一个名为T的类型参数。在compare中,我们用名字T表示一个类型。而T表示的实际类型则在编译时根据compare的使用情况来确定。

template<typename T>
int compare(const T&v1,const T &v2)
{
   if(v1<v2) return -1;
   if(v1>v2) return 1;
   return 0;
}

Note:在模板定义中,模板参数列表不能为空
  
  模板参数列表的作用很像函数参数列表。函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时,调用者提供实参来初始化形参。
  类似的,模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们(隐式地或显式地)指定 模板实参(template argument)并将其绑定到模板参数上
  
  函数模板的类型实参和非类型实参都可以由编译器从函数实参中推断

实例化函数模板

当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。即,当我们使用函数模板时,编译器根据实参的类型来推断绑定到模板参数的类型(即类型实参)。

例如,在下面的调用中:

cout<<compare(1, 0)<<endl;  // T为int

编译器推断出模板实参为 int

编译器用推断出的模板实参来 实例化(instantiate) 一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参替换对应的模板参数来创建出模板的一个新“实例”
  
  编译器生成的版本通常被称为 模板的实例(instantiation)

例如,给定下面的调用:

// 实例化出 int compare(const int&,const int&)
cout << compare(1,0) << endl; 	// T为int
// 实例化出 int compare(const vector<int>&,const vector<int>&)
vector<int> vec1(1,2,3), vec2(4,5,6);
cout << compare(vec1,vec2) << endl;	// T为vector<int>

对于第一个调用,编译器会编写并编译一个compare版本,其中T被替换为int;对于第二个调用,编译器会生成另一个compare版本,其中T被替换为vector。而这两个实际生成的函数就是前面模板的两个实例。

类型模板形参

模板函数有一个或多个类型模板形参(template type parameter)一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型模板形参可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换

  • 可以用内置(非类)类型作为模板类型实参。
  • 在模板参数列表中,每个模板参数前必须加上 class 或 typename。
      
      声明类型参数时,必须使用关键字 class 或 typename在模板参数列表中,这两个关键字的含义相同,可以相互转换使用。在一个模板参数列表中 可以同时使用 这两个关键字:
template <typename T, U> T calc1(const T&,const U&);	// 错误:U之前必须加上 class 或 typename
// 正确:在模板参数列表中,typename和class没有什么不同
template<typename T,class U> calc2(const T&,const U&);

非类型模板参数

除了定义类型参数,还可以在模板定义中定义非类型参数(nontype parameter)一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字 class 或 typename 来指定非类型参数。
  
  当一个模板被实例化时,非类型参数被一个 ①用户提供的值 或 ②编译器推断出的值 所代替这些值必须是常量表达式,从而允许编译器在编译时实例化模板

例如,可以编写一个 compare 版本处理字符串字面常量。这种字面常量是 const char 的数组。由于不能拷贝一个数组,所以我们将自己的参数定义为数组的引用。由于希望能比较不同长度的字符串子面常量,因此为模板定义了两个非类型的参数。第一个模板参数表示第一个数组的长度,第二个参数表示第二个数组的长度。

template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]){
	return strcmp(p1, p2);
}

compare("hi", "mom")这样调用compare时,编译器会实例化下面版本:
int compare(const char (&p1)[3], const char (&p2)[4])
  
  其中p1和p2的实际长度比内容大1,这是因为C++的字符串常量是C风格的字符串,所以会在内容"hi"和"mom"后添加空字符作为结尾。
  编译器推断出函数实参的两个类型分别是 const char (&)[3]const char (&)[4] ,其中的3和4是常量表达式,且分别对应了模板形参N和M,所以进行了替换。

  • 一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用等
  • 绑定到非类型整型参数的实参必须是一个常量表达式
  • 绑定到指针或引用非类型参数的实参必须具有静态的生存期。不能用一个普通( 非 static )局部变量或动态对象作为指针或引用非类型模板参数的实参。
  • 指针参数也可以用 nullptr 或一个值为 0 的常量表达式来实例化

在这里插入图片描述

inline 和 constextpr 的函数模板

函数模板同样可以声明为 inline 或 constexpr 的,和非模板函数一样。关键字 inline 或 constexpr 说明符放在模板参数列表之后,返回类型之前

// 正确:inline 说明符跟在模板参数列表之后
template <typename T> inline
T min(const T&, const T&);
// 错误:inline 说明符的位置不正确
inline template <typename T>
T min(const T&, const T&);

编写类型无关的代码

编写泛型代码有两个重要原则需要程序员遵循:

  • 模板中的函数形参是对 const 的引用;
  • 函数体中的条件判断仅使用 < 比较运算。

那为什么函数参数要是对 const 的引用?

  1. 因为将函数的参数设定为 const 引用可以保证该函数可以用于不能拷贝的类型( 比如:智能指针类型 unique_ptr 和 IO 类型)。
  2. 而且当该函数处理大的对象时,那么这种设计也会使函数运行得更快

那为什么仅使用 < 比较运算?

如果编写代码时只使用 < 运算符,我们就降低了该函数对要处理的类型的要求。这些类型必须支持 < ,但支不支持 > 并不重要。因为在之前的标准库容器和算法类的学习中可以知道,大部分标准库算法都使用到了类型的 < 运算符。

实际上,如果真正关心类型独立性和可移植性,可能需要使用标准库函数对象 less (参见14.8.2节)来定义函数。

// 即可用于内置指针也可
template <typename T> int compare(const T &v1, const T &v2) {
	if (less<T>()(v1, v2)) return -1;
	if (less<T>()(v2, v1)) return 1;
	return 0;
}

原始版本的问题是:如果用户调用它比较两个指向不同数组的指针,那么指针的比较行为是未定义的。

Note:模板程序应该尽量减少对实参类型的要求

模板编译

当编译器遇到一个模板的定义时,它不会产生代码。只有当实例化模板的一个特定版本时,编译器才会产生代码。当我们使用(而不是定义)模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到的时间。
  
  通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。
  
  模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包含模板的声明也包括定义

Note:函数模板和类模板成员函数的定义通常放在头文件中。

关键概念:模板和头文件
模板包含两种名字:

  • 那些不依赖于模板参数的名字。
  • 那些依赖于模板参数的名字。
template<class T> void test(const T& t){ cout << t; }	// test依赖于模板参数
void f1(){}	// f1不依赖于模板参数

模板的设计者要保证:
  ① 当使用模板时,所有不依赖模板参数的名字都必须可见的。
  ② 而且,当模板被实例化时,模板的定义,包括类模板的成员的定义,也必须是可见的。
  
模板用户要保证:
  用来实例化模板的东西(比如模板实参),在实例化模板的时候都必须是可见的。
  
模板的设计者:
  应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。
  
模板的使用者:
  必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。

大多数编译错误在实例化期间报告

在实例化模板时,编译器通常可能会在三个阶段标记错误:

  • 第一个阶段是编译模板本身时编译器可以检测到语法错误,比如忘记分号或变量名拼写错误等。
  • 第二个阶段是当编译器检测到模板被使用时
    • 对于函数模板的使用,编译器一般检查实参的数目是否正确。它还可以检查支持两个相同类型的两个实参类型是否相同
    • 对于类模板的使用,编译器可以检查模板实参的正确数目
  • 第三个阶段是模板实例化期间只有在这个阶段的时候,类型相关的错误才能被找到。依赖于编译器如何管理实例化,这类错误可能在链接时报告

当编写模板时,代码不是特定于类型的,但是模板代码通常会对要使用的类型做出一些假设

例如:
下面的代码就假设了传给模板形参类型 T 的实参类型支持 < 操作

if (v1 < v2) return -1;  // 要求类型 T 的对象支持 < 操作
if (v2 < v1) return 1;   // 要求类型 T 的对象支持 < 操作
return 0;                // 返回 int 值,不依赖于类型 T 

当编译器处理此模板时,它不能验证 if 语句中的条件是否合法。如果传递给 compare 实参有 < 操作,那么代码是正确的,否则是错误的:

Sales_data data1, data2;
cout << compare(data1, data2) << endl; // 错误:Sales_data 未定义 <

此调用实例化了模板函数 compare 的一个版本,将模板的类型参数 T 替换为Sales_data 。if 语句的条件试图对 Sales_data 调用运算符 < ,但该类型并未定义此运算符。则该实例化生成了一个编译无法通过的函数版本。但是,这样的错误 直至编译器在类型 Sales_data 上实例化 compare 时才会被发现

WARNING:
  调用模板的用户有责任保证传递给模板的实参支持模板使用的任何操作,并且在模板使用它们的代码中,这些操作行为是正确的

16.1.2 类模板

类模板(class template) 是用于生成类的蓝图。与函数模板不同的是编译器不能为类模板根据模板实参,来推断模板参数类型
  为了使用类模板,必须在尖括号内提供额外信息——用来替代模板形参的模板实参列表。
  类模板不是类,只有类模板被实例化后才是类。

定义类模板

类似函数模板,类模板的声明和定义以关键字 template 开始,后紧跟模板形参列表。在类模板(及其成员)的定义中,我们将模板形参当作替身,替代使用模板时用户需要提供的类型或值。
  当编译器实例化类模板时,模板的类型形参(非类型形参)将被替换为特定的模板类型实参(非类型实参)

例如,我们的 Blob 模板有一个名为 T 的模板类型参数,它用来表示 Blob 保存的元素的类型。当用户实例化 Blob 时,T 就会被替换为特定的模板实参类型。

template <typename T> class Blob {
public:
   typedef T value_type;
   // 必须且只能使用 typename 指出未确定成员 size_type 是一个类型
   typedef typename std::vector<T>::size_type size_type;
   // 构造函数
   Blob();
   Blob(std::initializer_list<T> i1);
   // Blob 中的元素数目
   size_type size() const { return data -> size(); }
   bool empty() const { return data -> empty(); }
   // 添加和删除元素
   void push_back(const T& t) { data -> push_back(t); }
   void push_back(T&& t) { data -> push_back(std::move(t)); }
   void pop_back();
   // 元素访问
   T& back();
   T& operator[](size_type i);

private:
   std::shared_ptr<std::vector<T>> data;
   void check(size_type i,const std::string &msg) const;
};

实例化类模板

当使用一个类模板时,我们必须提供额外信息——即, 显式模板实参(explicit template argument) 列表,它们被绑定到模板形参。编译器使用这些实参来实例化特定的类

  • 当编译器从某个类模板实例化出一个类时,它会重写该模板,将每个模板参数的每个实例替换为给定的模板实参。
  • 对用户指定的每一种元素类型,编译器都生成一个不同的类。

例如:

Blob<int> id;		// 空 Blob<int>
Blob<int> ia2 = {0,1,2,3,4};	// 含 5 个元素的 Blob<int>

对于 Blob 编译器便会实例化出一个与下面定义等价的类:

template <> class Blob<int> {
public:
   typedef typename std::vector<int>::size_type size_type;
   // 构造函数
   Blob();
   Blob(std::initializer_list<int> i1);
   // ...
   int& back();
   int& operator[](size_type i);

private:
   std::shared_ptr<std::vector<int>> data;
   void check(size_type i,const std::string &msg) const;
};

实际上就是重写 Blob 模板,将模板参数 T 每个实例替换为给定的模板实参,在本例中就是 int。

Note:

  • 一个类模板的每个实例都形成一个独立的类。对于同一个类模板的多个实例化生成的类,它们之间没有关联
  • 某一个实例化的类模板也不具有对其它实例的成员的特殊的访问权限

在模板作用域中引用模板类型

为了阅读模板类代码,应该记住类模板的名字不是一个类型名类模板是用来实例化类型的,而一个实例化的类型总是包含模板实参的
  
  ​可能令人迷惑的是,一个类模板中的代码如果使用了另一个模板,通常不将一个实际类型(或值)的名字用作其模板实参。相反,我们通常将 模板自己的模板形参 当作 被使用模板的实参

例如,我们的 data 定义如下:

std::shared_ptr<vector<T>> data;

它使用了 Blob 的类型参数来声明 data 是一个 shared_ptr 实例。当我们实例化一个特定类型的 Blob,例如 Blob<string> 时,data 会成为:

shared_ptr<vector<string>> data;

如果我们实例化 Blob<int> ,则 data 会成为 shared_ptr<vector<int>> ,依次类推。

类模板的成员函数

​​  与其它类相同,我们既可以在类模板内部,也可以在类模板外部为其定义函数,且定义在类模板内的成员函数被隐式声明为内联函数
  
​  类模板的成员函数本身是一个普通函数。但是,类模板的每个实例都有自己版本的成员函数。因此,定义在类外部的函数必须以关键字 template 开始,后接类模板参数列表
​  
​  同时,在类模板外定义成员时,仍然需要说明该成员属于哪个类。而且,从一个类模板中生成的类的名字中必须包含其模板实参。当我们在类模板外定义一个成员函数时,它的模板实参与模板形参相同。

所以类模板 Blob 的类外定义的成员的格式应该是下面这样:

// T是类模板的形参,在这里又作为实参
template<typename T>
	ret_type Blob<T>::member_name(parm-list)

上面<typename T>的 T 是类型形参,Blob<T>中的 T 是类型实参。

Blob的构造函数

​  同样的,类模板的构造函数在类外的定义和普通函数一样,要以 template 关键字开始

template <typename T>
	Blob<T>::Blob():data(std::make_shared<std::vector<T>>()) { }

template <typename T>
	Blob<T>::Blob(std::initializer_list<T> i1):data(std::make_shared<std::vector<T>>(i1)) { }

类模板实例的成员函数、成员类和 static 数据成员的实例化

​  默认情况下一个类模板的某个实例成员类static 数据成员成员函数(无论是否是函数模板)通常只有当程序用到它时才进行实例化。 除非该类模板的实例是显式实例化的 例如,下面代码:

// 实例化 Blob<int> 和接受 initializer_list<int>的构造函数
Blob<int> squares = {0,1,2,3,4,5,6,7,8,9};
// 实例化 Blob<int>::size() const
for(size_t i = 0;i != squares.size();++ i)
    square[i] = i * i;	// 实例化 Blob<int>::operator[](size_t)

​  如果一个类模板的某个实例的成员类、成员函数或 static 数据成员没有被使用,则它通常不会被实例化。

​  成员类、成员函数和 static 数据成员只有在被用到时才进行实例化,这一特性使得——即使某种类型不能完全符合模板操作的要求(比如被该类型实例化的成员函数只声明未定义),我们仍然能用该类型实例化类

在类代码内简化模板类名的使用

​  一般当我们使用一个类模板类型时必须提供模板参数,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不需要提供模板实参

// 若试图访问一个不存在的元素,BlobPtr 将抛出异常
// 当然,我们的 BlobPtr 应该为 Blob 的友元类。
// 声明方式:template<typename> class BlobPtr;
template <typename T> class BlobPtr {
public:
    BlobPtr(): curr(0) { }
    BlobPtr(Blob<T> &a,size_t sz = 0):
        wptr(a.data), curr(sz) { }
    T& operator*() const {
        auto p = check(curr,"dereference past end");
        return (*p)[curr];
    }
    BlobPtr&operator++();		// 前置
    BlobPtr&operator--();
    BlobPtr&operator++(int);	// 后置
    BlobPtr&operator--(int);
private:
    std::size_t curr;       // 数组中的当前位置
    std::weak_ptr<std::vector<T>> wptr;
    // 若检查成功,check 返回一个指向 vector 的 shared_ptr
    std::shared_ptr<std::vector<T>>
        check(std::size_t t,const std::string&msg) const;
};
template<typename T>
std::shared_ptr<std::vector<T>> BlobPtr<T>::check(std::size_t t,const std::string &msg) const {
    auto ret = wptr.lock();
    if(!ret) throw std::runtime_error("unbound StrBlobPtr");
    if(t >= ret -> size())
        throw std::out_of_range(msg);
    return ret;
}

​  可以发现,BlobPtr 的前置递增和递减返回的是 BlobPtr& ,而不是 BlobPtr<T>&当我们处于一个模板类的作用域中时,编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。

在类模板外使用类模板名

​  当我们在类模板外定义其成员时,必须记住,我们不在类的作用域中直至遇到类模板名才表示进入类模板的作用域

// 前置递增/递减
template <typename T> 
BlobPtr<T> BlobPtr<T>::operator++(int){	
	// 返回值类型 BlobPtr<T> 不在作用域内,所以不能省略<T>
	// 下面的 BlobPtr 在作用域内,所以能省略<T>
    BlobPtr ret = *this;	
    ++*this;
    return ret;
}

​  由于返回类型位于类的作用域外,所以必须指出返回类型是一个实例化的 BlobPtr ,它所用的类型和类实例化所用类型一致。在函数体内的时候,我们已经进入了类的作用域,因此在定义 ret 时无需提供模板实参,编译器将假定我们使用的类型与用户实例化所用的类型一致。因此 ret 的定义与下面代码等价:

BlobPtr<T> ret = *this;

类模板和友元

​  当一个类包含一个友元声明时,类与友元各自是否是模版是无关的。

  • 如果一个模板类包含一个非模板友元,则友元被授权可以访问该模板的所有实例。
  • 如果友元自身是模板,类既可以授权给所有友元模板实例,也可以只授权给特定实例。

一对一的友元关系

​  类模板与另一个模板间友好关系的最常见的形式是:建立对应实例及其友元间的友好关系
​  
​  注意:为了引用(类或函数)模板的一个特定实例,我们必须首先在类外的之前声明模板自身

​  一个模板声明包括模板形参列表

// 前置声明,声明友元的必要操作
template<typename T> class BlobPtr;
template<typename T> class Blob;
template<typename T>
	bool operator==(const Blob<T>&, const Blob<T>&);
	
template<typename T> class Blob {
    // 每个 Blob 实例将访问权限授予用相同类型实例化的 BlobPtr 与相等运算符
    friend class BlobPtr<T>;
    friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
    // 其他的成员与之前相同
};

​  友元的声明用 Blob 的模板形参作为它们自己的模板实参。因此,友元关系被限定在用相同类型实例化的 Blob 和 BlobPtr 相等运算符之间

Blob<char> ia;	// BlobPtr<char> 和 operator==<char> 都是本对象的友元
Blob<int> ia;	// BlobPtr<int> 和 operator==<int> 都是本对象的友元

通用和特定的模板友元关系

​​  一个类也可以将另一个模板的每个实例都声明为自己的友元,或者指定特定的实例为友元

// 前置声明,在将模板的一个特定实例声明为友元时要用到
template <typename T> class Pal;

class C {		// C 是一个普通的非模板类
    friend class Pal<C>;	// 用类 C 实例化的 Pal 是 C 的一个友元
    // Pal2 的所有实例都是 C 的友元;这种无需前置声明
    template <typename T> friend class Pal2;
};
template <typename T> class C2 {		// C2 本身是一个类模板
    // C2 的每个实例将相同实例化的 Pal 声明为友元
    friend class Pal<T>;	// Pal 的模板声明必须在作用域之内
    // Pal2 的所有实例都是 C2 每个实例的友元;不需要前置声明
    template <typename X> friend class Pal2;
    // Pal3 是一个非模板类,它是 C2 所有实例的友元
    friend class Pal3;		// 不需要 Pal3 的前置声明
};

​  为了让某个模板的所有实例成为友元,友元声明中必须使用与类模板本身形参不同的模板参数

通用和特定模板的友元关系需要注意一点:

  • 如果要引用(类或函数)模板的特定实例作为友元,我们必须在类外之前先声明模板自身;
  • 如果要引用(类或函数)模板的所有实例作为友元,则不需要在类外之前先声明模板自身。

令模板自己的类型参数成为友元

​  在C++11中,我们可以将模板的类型参数声明为友元

template <typename Type> class Bar {
friend Type;	// 将访问权限授予用来实例化 Bar 的类型
    // ...
};

​  此处我们将用来实例化 Bar 的类型声明为友元。即对于某个类型名 S ,它将会成为 Bar<S> 的友元,依次类推。

​  值得注意的是,虽然友元通常来说是一个类或函数,但我们完全可以用内置类型来实例化类模板,这种与内置类型的友好关系是允许的,以便我们能用内置类型来实例化类模板

模板类型别名

​  类模板的一个实例定义了一个类类型。与任何其他类类型一样,我们可以用关键字 typedef 定义一个类型别名来引用被实例化的类模板,比如 string 就是 basic_string<char> 的类型别名:

typedef Blob<string> StrBlob;
StrBlob x;			// x 为 Blob<string>

​  由于(类或函数)模板不是一个类类型或函数,我们不能用关键字 typedef 定义一个类型别名来引用一个类模板。即,无法定义一个 typedef 引用 Blob<T>
​  但是,C++11 允许使用关键字 using 为类模板定义一个类型别名

template<typename T> using twin = pair<T, T>;
twin<string> authors;// authors 是 pair<string,string>

​  当我们用 using 定义一个模板类型别名时,可以使用,可以用具体的类型或值,来固定一个或多个模板参数

template<typename T> using partNo = pair<T,unsigned>;
partNo<string> books;		// books 是 pair<string,unsigned>
partNo<double> s;	// s 是 pair<double,unsigned>

类模板的 static 成员

​  与任何其它类相同,类模板可以声明 static 成员

template<typename T> class Foo {
public:
    static std::size_t count() { return ctr; }
private:
    static std::size_t ctr;
};

​  在这段代码中, Foo 是一个类模板,它有一个名为 count 的 public static 成员函数和一个名为 ctr 的 private static 数据成员。每个 Foo 的实例都有其自己的 static 成员实例。即,对任意的给定类型 X,都有一个 Foo<X>::ctr 和一个 Foo<X>::count 成员。所有 Foo<X> 类型的对象都共享相同的 ctr 和 count。例如:

Foo<int> fi,fi2,fi3;		// 这三个对象共享相同的 Foo<int>::ctr 和 Foo<int>::count

​  与任何其它 static 数据成员相同,模板类的每个 static 数据成员必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的 static 对象,它被该实例的全部对象共享
​  所以,与在类模板外定义模板的成员函数类似,当需要在类外初始化类模板的 static 数据成员时,我们将 static 数据成员也定义为模版(成员函数也是)

template<typename T> 
size_t Foo<T>::ctr = 0;		// 定义并初始化 ctr

​  定义的开始部分是模板的参数列表,随后是我们定义的成员的类型和名字。与往常一样,成员名包括成员所属的类名,对于从模板实例化的类,类名还包括模板实参。因此,当使用一个特定的模板参数类型实例化 Foo 时,将会为该类类型实例化一个独立的 ctr ,并将其初始化为 0。

​  与非模板类的静态成员相同,我们既可以通过类类型对象来访问一个类模板的 static 成员,也可以使用作用域运算符直接访问成员。当然,为了通过类名来直接访问 static 成员,我们必须引用一个特定的实例

Foo<int> fi;	// 实例化 Foo<int> 类和 static 数据成员 ctr
auto ct = Foo<int>::count();// 实例化 Foo<int>::count
ct = fi.count();			// 使用 Foo<int>::count
ct = Foo::count();			// 错误,使用哪个模板实例的 count

​  类似类模板的其他成员函数,一个类模板的 static 成员只有在使用时才会实例化

16.1.3 模板参数

​  类似函数参数的名字,一个模板参数的名字也没有什么内在含义。我们通常将类型参数命名 T,但实际上我们可以使用任何名字

template<typename Foo> Foo calc(const Foo& a,const Foo &b) {
    Foo tmp = a;
    // ...
    return tmp;
}

模板参数与作用域

​  模板参数 遵循 普通的作用域规则

  • 一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前
  • 与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字
  • 但是,与大多数其他上下文不同,在模板内不能重用模板参数名
typedef double A;
template<typename A, typename B> void f(A a,B b) {
    A tmp = a;		// tmp 的类型为模板参数 A 的类型,而非 double
    double B;		// 错误,重声明 B 
}

​  由于模板参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次

template<typename A,typename A>		// 非法重用模板参数名 A

模板声明

​  模板声明必须包含模板参数

// 声明但不定义 compare 和 Blob
template<typename T> int compare(const T&,const T&);
template<typename T> class Blob;

​  与函数参数类似,声明中的模板参数的名字不必与定义中的相同:

// 以下三个 calc 都指向相同的函数模板	
// 多次声明是合法的
template <typename T> T calc(const T&,const T&);	// 声明
template <typename U> U calc(const U&,const U&);	// 声明
// 定义
template <typename E>
E calc(const E &a,const E &b) { /* */ }

​  当然,一个给定模板的每个声明和对应定义,必须有相同数量和种类(即,类型或非类型)的参数

Best Practices一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。原因将在16.3节中解释。

使用类的类型成员( typename 和 class 在模板中的唯一区别)

​  在普通(非模板)代码中,编译器知道类的定义。编译器知道通过作用域运算符访问的名字是类型还是 static 成员。例如,string::size_type,编译器有 string 的定义,编译器知道 size_type 是一个类型而非成员。

​  但是对于模板代码就存在困难。例如,假定 T 是一个模板类型参数,当编译器遇到类似 T::mem 的代码时,它不会知道 mem 是一个类型成员还是 static 数据成员,直到实例化时才会知道。像 mem 这样的名字就被称为依赖名。

​  依赖名(dependent name):在模板(类模板和函数模板)定义中,某些构造的含义可以在不同的实例化间有所不同。特别是,类型和表达式 可以取决于 类型模板形参的类型和非类型模板形参的值。

​  但是,为了处理模板,编译器必须知道依赖名是否表示一个类型。例如,假定 T 是一个类型参数的名字,当编译器遇到如下形式的语句:

T::size_type * p;

​  只有当编译器知道 T::size_type 是类型成员时,上面语句才翻译为 p 是一个 T::size_type 类型的指针 ,否则编译器会理解为这是一个 T::size_type 中的 static 数据成员与 p 相乘。

​​  默认情况下, C++ 假定通过作用域运算符访问的名字不是类型。 所以,我们需要使用 typename 来达到以上目的:

// 常见的第一种情况
template <typename T>
typename vector<T>::size_type top(const T &c) {	// 这里的 T 因为是类型模板参数,所以肯定是类型而不是static变量
    /* ... */
}
// 常见的第二种情况
template <typename T>
typename vector<T>::size_type top(const typename T::c &c) {
    /* ... */
}

​  当我们希望 告知编译器一个依赖名表示类型时,必须使用关键字 typename ,不能使用 class

默认模板实参

​  就像为函数参数提供默认实参一样,我们也可以在模板参数列表中提供默认模板实参(default template argument)

​  在 C++11 中,可以为函数模板和类模板提供默认实参。在更早的版本只允许为类模板提供默认实参

​  例如,默认使用标准库的 less 函数对象版本来编写 compare :

template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F()) {
    if(f(v1,v2)) return -1;
    if(f(v2,v1)) return 1;
    return 0;
}

在这段代码中,我们为模板添加了第二个类型参数 F ,表示可调用对象(参见10.3.2节)的类型;并定义了一个新的函数参数 f ,绑定到一个可调用对象上。

​  我们为此模板参数提供了默认实参,并为其对应的函数参数也提供了默认实参。默认模板实参指出 compare 将使用标准库的 less 函数对象类,它是使用与 compare 一样的类型参数实例化的。默认函数实参指出 f 将是类型 F 的一个默认初始化的对象。

​  当用户调用这个版本的 compare 时,可以提供自己的比较操作,但这并不是必需的:

bool i = compare(0, 42);	// 使用 less ;  i 为-1
// 结果依赖于item1和item2中的isbn
Sales_data item1(cin), item2(cin) ;
bool j = compare (item1,item2,compareIsbn) ;

第一个调用使用默认函数实参,即,类型 less<T> 的一个默认初始化对象。在此调用中, T 为 int ,因此可调用对象的类型为 less 。 compare 的这个实例化版本将使用less进行比较操作。

​  在第二个调用中,我们传递给 compare 三个实参: compareIsbn (参见11.2.2节)和两个 Sales_data 类型的对象。当传递给 compare 三个实参时,第三个实参的类型必须是一个可调用对象,该可调用对象的返回类型必须能转换为bool值,且接受的实参类型必须与compare的前两个实参的类型兼容。与往常一样,模板参数的类型从它们对应的函数实参推断而来。在此调用中, T 的类型被推断为 Sales_data , F 被推断为 compareIsbn 的类型。

​  与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以被提供默认实参。

  • 若为主类模板、主变量模板(C++14 起) 或别名模版的模板形参指定默认实参,则其每个后继模板形参都必须有默认实参,但最后一个可以是模板形参包。在函数模板中,对跟在默认实参之后的形参没有限制,而仅当类型形参具有默认实参,或可从函数实参推导时,才可跟在形参包之后。

以下情况不允许默认形参

  • 在类模板的成员的类外定义中(必须在类体内的声明中提供它们)。注意非模板类的成员模板可以在其类外定义中使用默认形参(见 GCC 漏洞 53856)
  • 在友元类模板声明中

模板默认实参与类模板

​  无论何时使用一个类模板,我们都必须在模板名之后接上尖括号 <> 尖括号指出类必须从一个模板实例化而来
​  特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对

template <class T = int>
class Numbers{ // T默认为int
public:
	Numbers(T v = 0): val(v) { }	// 对数值的各种操作
private:
	Tval;
};

Numbers<long double> lots_of_precision;
Numbers<> average_precision; 		//空<>表示我们希望使用默认类型

​  此例中我们实例化了两个 Numbers 版本: average_precision 是用 int 代替 T 实例化得到的; lots_of_precision 是用 long double 代替 T 实例化而得到的。

16.1.4 成员模板

​  一个类 (普通类或类模板) 可以包含 本身是模板的函数或类 作为成员——这种模板被称为成员模板(member template)

​  成员模板不能虚函数

​  成员模板的声明和定义也是以模板参数列表开始

普通(非模板)类的成员模板

​  作为普通类包含成员模板的例子——我们定义一个类,类似 unique_ptr 所使用的默认删除器类型。类似默认删除器,我们的类将包含一个重载的函数调用运算符,它接受一个指针并对此指针执行 delete 。与默认删除器不同,我们的类还将在删除器被执行时打印一条信息。由于希望删除器适用于任何类型,我们将调用运算符定义为一个模板:

// 函数对象类,对给定指针执行 delete
class DebugDelete {
public:
    DebugDelete(std::ostream &s = std::cerr): os(s) { }
    // 与任何函数模板相同,T 的类型由编译器推断
    template<typename T> void operator()(T* p) const {
        os << "deleting unique_ptr" << std::endl; 
        delete p;
    }
private:
    std::ostream &os;
};

​  与任何其它的模板相同,成员模板的声明和定义也是以模板参数列表开始。我们可以用这个类代替 delete 表达式:

double *p = new double;
DebugDelete d;		// 可像 delete 表达式一样使用对象
d(p);				// 调用 DebugDelete::operator()(double*),释放 p
int *ip = new int;
DebugDelete()(ip);	// 在一个临时的 DebugDelete 对象上调用 operator()(int*)

​  由于 DebugDelete 会 delete 给定的指针,我们也可以将 DebugDelete 作为 unique_ptr 的删除器。为了重载 unique_ptr 的删除器,我们在尖括号内给出删除器类型,并提供一个这种类型的对象给 unique_ptr 的构造函数:

// 销毁 p 指向的对象
// 实例化 DebugDelete::operator()<int>(int*)
unique_ptr<int, DebugDelete> p(new int,DebugDelete());
// 销毁 Sp 指向的对象
// 实例化 DebugDelete::operator()<string>(string*)
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());

​  在本例中,我们声明 p 的删除器类型为 DebugDelete,并在 p 的构造函数中提供了该类型的一个未命名对象。

​  unique_ptr 的析构函数会调用 DebugDelete 的调用运算符。因此,无论何时 unique_ptr 的析构函数实例化时,DebugDelete 的调用运算符都会实例化:因此,上述定义会下面这样实例化两个函数:

void DebugDelete::operator() (int *p) const { delete p; }
void DebugDelete::operator() (string *p) const { delete p; }

类模板的成员模板

​  对于类模板,我们也可以为其定义成员模板在此情况下,类模板和成员模板各自拥有自己的、独立的模板参数

​  ​例如,我们为一个类模板 Blob 添加一个构造函数,它接受两个迭代器,表示要拷贝的元素范围。由于我们希望支持不同类型序列的迭代器,因此将构造函数定义为模板:

template<typename T> class Blob {
	// 因为在作用域内,所以可以省略 template<typename T> 和 Blob<T>::
    template<typename It> Blob(It b,It e);	
    // ...
};

​  ​与类模板的普通成员不同,成员模板是模板。所以,当我们在类外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表类模板的模板参数列表在前,后紧跟成员自己的模板参数列表,然后就是 类名::成员

template<typename T>	// 类的模板参数
template<typename It>	// 构造函数的模板参数
	Blob<T>::Blob(It b,It e): data(std::make_shared<std::vector<T>>(b,e)) { }
// std::shared_ptr<std::vector<T>> data;

​  因为 普通的作用域规则 ,在第一段代码中,因为在类模板的作用域中,所以可以省略那部分

实例化与成员模板

​  为了实例化一个类模板的成员模板,我们必须同时提供类模板和成员模板的实参

  • 在哪个对象上调用成员模板编译器就根据该对象的类型来推断类模板参数的实参
  • 与普通的函数模板相同,编译器通常根据传递给成员模板的函数实参,来推断它的模板实参
int ia[] = {0,1,2,3,4,5,6,7,8,9};
vector<long> vi = {0,1,2,3,4,5,6,7,8,9};
list<const char*> w = {"now","is","the","time"};
// 实例化 Blob<int> 类及其接受两个 int* 参数的构造函数
Blob<int> a1(begin(a1),end(a1));
// 实例化 Blob<int> 类及其接受两个 vector<long>::iterator 参数的构造函数
Blob<int> a2(vi.begin(),vi.end());
// 实例化 Blob<string> 类及其接受两个 list<const char*>::iterator 参数的构造函数
Blob<string> a3(w.begin(),w.end());

16.1.5 显式(控制)实例化

​  模板本身不是类型、对象或任何其它实体,所以不会只从仅含模板的定义的源文件中生成任何代码

​  一般只有当模板被使用时才会进行实例化,这一特性意味着,相同的实例可能出现在多个对象文件中。两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例

​  在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在 C++11 中,我们可以通过 显式实例化(explicit instantiation) 来避免这种开销。

​  模板的显式实例化有以下形式(声明和定义):

extern template declaration;	// 实例化声明
template declaration;			// 实例化定义

​  declaration一个类模板或函数模板声明,其中所有模板参数也都被替换为模板实参。例如:

extern template class Blob<string>;		// 声明
template int compare(const int&,const int&);	// 定义

​  当编译器遇到 extern 模板声明时,它不会在本文件中生成实例化代码将一个实例化声明为 extern 就表示承诺在程序其他位置有该实例化的一个非 extern 声明(定义)对于一个给定的实例化版本,可能有多个 extern 声明,但必须只有一个定义

​  由于编译器在使用一个模板时自动对其实例化,因此 extern 声明必须出现在任何使用此实例化版本的代码之前

// Application.cc
// 这些模板类型必须在程序其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int&,const int&);
Blob<string> sa1, sa2;		// 实例化会出现在其他位置

Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};	// Blob<int> 以及接受 initializer_list 的构造函数在本文件中实例化
Blob<int> a2(a1);	// 拷贝构造函数在本文件中实例化
int i = compare(a1[0],a2[0]);		// 实例化出现在其他位置

​  我们在 templateBuild.cc 中实例化上述模板,并定义:

// templateBuild.cc
// 实例化文件必须为每个在其他文件中声明为 extern 的类型和函数提供一个 (非 extern) 的定义
template int compare(const int&,const int&);
template class Blob<string>;		// 实例化类模板的所有成员

​  当编译器遇到一个实例化定义(与声明相对),它为其生成实例化的代码。因此,文件 templateBuild.o 将会包含 compare 的 int 实例化版本的定义和 Blob 类的定义。 当我们编译此应用程序时, 必须将 templateBuild.o 和 Application.o 链接到一起。

Note:对每一个实例化声明,在程序中某个位置必须由其显式的实例化定义。

模板的实例化定义会实例化其所有成员

​  一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数当编译器遇到一个实例化定义时,它不了解程序使用哪些成员函数(因为类模板的实例化定义并没有指出将会使用哪个成员函数)。因此,与处理类模板的普通实例化不同,遇到类模板的显式实例化时,编译器会实例化该类的所有成员。即使我们不使用某个成员,它也会被实例化。因此,我们用来显式实例化一个类模板的类型,它必须能用于类模板的所有成员而不能像之前一样只适用于类模板中将被使用的(即将被实例化的)成员函数

模板的隐式实例化

函数模板的隐式实例化:
​  当代码在要求存在函数定义的上下文中引用某个函数模板时,(或若定义存在影响程序语义, C++11 起),而这个特定的函数尚未被显式实例化时,发生隐式实例化。若模板实参列表能从函数中推导,则不必提供它。

类模板的隐式实例化:
​  当代码在需要完整定义的类型的上下文中引用某个模板时,或当类型的完整性对代码有影响,而且该特定类型尚未被显式实例化时,则会发生隐式实例化。例如,当构造此类型的对象时,而不是当构造指向此类型的指针时。
​  这也适用于类模板的成员类、成员函数和 static 数据成员:除非这三种成员在程序中被使用,否则并不会实例化它,并且不要求其有定义。

16.1.6 效率和灵活性

​  对模板设计者所面对的设计选择,标准库的智能指针类型给出了一个很好的展示。
​  shared_ptr 和 unque_ptr 之间的明显不同是它们管理所保存的指针的策略——前者给予我们共享指针所有权的能力;后者则独占指针。
​  这两个类的另一个差异是它们允许用户重载默认删除器方式。我们可以很容易地重载一个 shared_ptr 的删除器,只要在创建或 reset 指针时传递给它一个可调用对象即可。与之相反,删除器的类型是一个 unique_ptr 对象类型的一部分。用户必须在定义 unique_ptr 时以显式模板实参的形式提供删除器类型。因此,对于 unique_ptr 的用户来说,提供自己的删除器就更为复杂。

​  如何处理删除器的差异实际上就是这两个类功能的差异。但是,如我们将要看到的,这一实现策略上的差异可能对性能有重要的影响。

在运行时绑定删除器(shared_ptr 删除器的工作方式)

​  虽然我们不知道标准库的具体实现(由编译器实现),但可以推断出shared_ptr 必须能直接访问其删除器。即,删除器必须保存为一个指针或一个封装了指针的类(如标准库 function 类,参见 14.8.3 节)

​  我们可以确定 shared_ptr 不是将删除器直接保存为一个成员,因为删除器的类型要到运行时才会知道。实际上,在一个 shared_ptr 的生存期中,我们可以随时改变其删除器的类型。我们可以使用一种类的删除器构造一个 shared_ptr,随后使用 reset 赋予此 shared_ptr 另一种类型的删除器。通常,类成员的类型在运行时是不能改变的。因此, shared_ptr 不能直接保存删除器

​  为了考察删除器是如何正确工作的,我们假定 shared_ptr 将它所管理的指针保存在一个成员 p 中,且删除器是通过一个名为 del 的成员来访问的。则 shared_ptr 的析构函数必须包含类似以下语句:

// del 的值只有在运行时才知道;通过一个指针来调用它
del ? del(p) : delete p;	// del 是否绑定了一个删除器,是则调用()运算符,否则使用 delete 表达式

​  由于删除器是间接保存的,调用 del(p) 需要一次运行时的跳转操作,转到 del 中保存的地址来执行对应的代码。

在编译时绑定删除器(unique_ptr 删除器的工作方式)

​  现在,让我们来考察 unique_ptr 可能的工作方式。在这个类中,删除器类型是类类型的一部分。即,unique_ptr 有两个模板参数,一个表示它所管理的指针,另一个表示删除器类型。由于删除器的类型是 unique_ptr 的一部分,因此删除器成员的类型在编译时是知道的,从而删除器可以保存在 unique_ptr 对象中

​  unique_ptr 的析构函数与 shared_ptr 的析构函数类似,也是对其保存的指针调用用户提供的删除器或执行 delete:

// del 在编译时绑定;直接调用实例化的删除器
del(p);		// 无运行时额外开销

del 的类型或者是默认删除器类型,或者是用户提供的类型。到底是哪种情况没有关系,应该执行的代码在编译时肯定知道。实际上,如果删除器是类似于 DebugDelete (参见 16.1.4 节)之类的东西,调用可能会被编译为内联形式。

​  通过在编译时绑定删除器,unique_ptr 避免了间接调用删除器的运行时开销。通过在运行时绑定删除器,shared_ptr 使用户重载删除器更为方便


16.2 模板实参推断(template argument deduction)

​  我们知道,对于函数模板,编译器利用函数调用中的函数实参来推断其模板参数(类型参数和非类型参数都是)。
​  函数实参来确定模板实参的过程被称为模板实参推断(template argument deduction)。在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配。

16.2.1 类型转换与模板的类型参数

​  与非模板函数一样,我们在一次函数调用中传递给函数模板的实参被用来初始化函数形参。如果一个函数形参的类型使用了模板类型参数,那么采用特殊的初始化规则——只有很有限的几种类型转换会自动的应用于这些实参。编译器通常不是对实参类型进行转换,而是生成一个新的模板实例

​  如果一个函数形参的类型使用了模板类型参数,顶层 const 无论在形参还是实参中,都会被忽略在其他类型转换中,能在调用中应用于函数模板的包括以下两项

  • const 转换:可以将一个非 const 对象的引用(或非 low-const 指针)传递给一个 const 引用(或 low-const 指针)形参(参见4.11.2节)。
  • 数组或函数指针的转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针(参见4.11.2节)。

​​  其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换都不能应用于使用了模板的类型参数或非类型参数的函数参数

​​  下面是一个例子:

template <typename T> T fobj(T, T);		// 拷贝
template <typename T> T fref(const T&, const T&);	// 引用

string s1("a value");
const string s2("another val");
fobj(s1, s2);		// 调用 fobj(string, string),实参的顶层 const 被忽略
fref(s1, s2);		// 调用 fref(const string&, const string&),形参的顶层 const 被忽略
					//将 s1 转换为 const 是允许的
int a[10],b[42];
fobj(a, b);			// 调用 fobj(int*, int*);
fref(a, b);			// 错误:数组类型不匹配,这里是引用, T 不能既是 int[10] 又是 int[42]

​  在 fobj 中,数组会自动转换为指针。但是在 fref 中,因为模板形参是引用,所以最后一个调用是错误的。

使用相同模板参数类型的函数形参

​​  一个模板类型参数可以用作多个函数形参的类型如果编译器根据实参推断出的类型(不是实参原来的类型)不匹配,则调用就是错误的

​  ​如我们有:

template <typename T> int compare(const T&, const T&);

​  当我们的调用方式为:

long lng;
compare(lng, 1024);	// 错误:不能实例化compare(long, int)

​  这是错误的,因为 lng 是 long 类型,是 1024 是 int 类型,而且无法使用算术类型的隐式转换。所以该调用无法实例化 compare 函数。

​  ​如果想要允许对函数实参的类型转换(不是指在用函数实参初始化函数形参时的转换,而是指形参被初始化后被使用时),我们可以为函数模板定义两个类型参数

template <typename A, typename B> 
int flexibleCompare(const A& v1,const B& v2){
	if (v1 < v2) return -1;
	if (v2 < v1) return 1;
	return 0;
}

​  这样,便可以提供不同类型的实参。如上述调用是合法的。当然了,必须定义了能比较这些类型的值的 < 运算符,因为这里用到了。

正常类型转换应用于普通函数实参

​  在函数模板的函数参数列表中,可以有非模板类型参数定义的参数,即不涉及模板类型参数的类型。这种函数参数不进行特殊处理;它们正常使用普通函数的参数转换规则。如下函数模板:

template <typename T> ostream &print(ostream &os,const T &obj) {
    return os << obj;
}

​  我们可以看见,该函数模板的第一个参数是 ostream& ,所以当我们调用此函数时,传递给它的实参会进行正常的类型转换:

print(cout, 42);		// 实例化 print(ostream&, int);
ofstream f("output");
print(f,10);			// 使用 print(ostream&, int); f 转换为 ostream&

16.2.2 函数模板显式实参

​  在某些情况下,编译器无法推断出模板实参的类型在其他一些情况下,我们希望允许用户控制模板实例化当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现

指定显式模板实参

​  作为一个允许用户指定使用类型的例子,我们将定义一个名为 sum 的函数模板,它接受两个不同类型的参数。我们希望允许用户指定结果的类型。这样,用户就可以选择合适的精度。

​​  我们可以定义表示返回类型的第三个模板参数,从而允许用户控制返回类型:

// 编译器无法推断 T1,它未出现在函数参数列表中
template <typename T1,typename T2,typename T3>
T1 sum(T2, T3);

​  编译器无法推断 T1,因为它未出现在函数参数列表中。所以每次调用 sum 时调用者必须为 T1 提供一个显式模板实参(explicit template argument)

​​  给函数模板提供显式模板实参的方式与定义类模板实例的方式相同在函数名之后,实参列表之前用尖括号指明

auto val3 = sum<long long>(i, lng);		// long long sum(int,long);

​  ​显式模板实参按由左至右的顺序与对应的模板参数匹配,即第一个模板实参与第一个模板参数匹配,依此类推。只有后端 (右边) 参数的显式模板实参才可以忽略,前提是它们可以从函数参数推断出来或具有默认实参

​  ​如我们的 sum 函数如下编写:

template <typename T1,typename T2,typename T3>
T3 alternative_sum(T2,T1);

​  我们总是必须提供三个形参指定实参:

auto val3 = alternative_sum<long long>(i, lng);		// 错误,T3 的类型未知
auto val2 = alternative_sum<long long, int, long>(i, lng);	// 正确,显式指定了三个参数

正常类型转换应用于显式指定的实参

​  对于用普通类型定义的函数参数,允许进行正常的类型转换。出于同样的原因对于模板类型参数已经显式指定了的函数参数,或是具有默认实参的函数参数,也将进行正常的类型转换

// template <typename T> int compare(const T&,const T&);
long lng;
compare(lng,1024);			// 错误,模板参数不匹配
compare<long>(lng,1024);	// 正确,实例化 compare(long,long)
compare<int>(lng,1024);		// 正确,实例化 compare(int,int)

16.2.3 尾置返回类型于类型转换

​  当我们希望用户确定返回类型时,用显式模板实参表示模板函数的返回类型时很有效的。但在其他情况下,要求显式指定模板实参会给用户增添额外负担。例如,我们可能希望编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用:

template <typename It>
??? &fcn(It beg,It end) {	// ??? 表示不知道返回类型,这是伪代码
    // 处理序列
    return *beg;
}

​  我们 不知道返回结果的精确类型,但知道所需类型是所处理的序列的元素类型(即返回类型可从函数参数中间接或直接获取

vector<int> vi = {1,2,3,4,5};
Blob<string> ca = {"hi","bye"};
auto &i = fcn(vi.begin(),vi.end()); // fcn 应该返回 int&
auto &j = fcn(ca.begin(),ca.end()); // fcn 应该返回 string&

​  我们知道,函数返回 *beg ,而我们能够通过 decltype(*beg) 来获取此表达式的类型。但是,在编译器遇到函数的参数列表之前,beg 都是不存在的。所以,为了定义此函数,我们必须使用尾置返回类型。由于尾置返回类型出现在参数之后,它可以使用函数的参数

template <typename It>
auto fcn(It beg,It end) -> decltype(*beg) {
    // 处理序列
    return *beg;		// 返回序列中一个元素的引用
}

进行类型转换的标准库类模板

​  有时我们无法直接获得所需要的类型。例如,我们可能希望编写一个类似 fcn 的函数,它返回一个元素的值而非引用。

​​  在编写这个函数的过程中,我们面临一个问题:对于传递的参数的类型,我们几乎一无所知在此函数中,我们知道唯一可以使用的操作是迭代器操作,而所有迭代器操作都不会生成元素,只能生成元素的引用

​​  为了获得元素类型,我们可以使用标准库的类型转换模板。这些模板定义在头文件 type_traits。下表列出了这些模板,我们将在 16.5 节中看到它们的关键实现。

标准类型转换模板
对 Mod< T > 其中 Mod 为若 T 为则 Mod< T >::type 为
remove_reference X& 或 X&&
否则
X
T
add_constX&, const X或函数
否则
T
const T
add_lvalue_referenceX&
X&&
否则
T
X&
T&
add_rvalue_referenceX&或X&&
否则
T
T&&
remove_pointerX*
否则
X
T
add_pointerX&或X&&
否则
X*
T*
make_signedunsigned X
否则
X
T
make_unsigned带符号类型
否则
unsigned X
T
remove_extentX[n]
否则
X
T
remove_all_extent X[n1][n2]
否则
X
T

​​  在本例中,我们可以用标准库的类型转换的类模板 remove_references 来获得元素类型。 remove_references 有一个模板类型参数和名为 type 的 public 类型成员。我们用给一个引用类型实例化 remove_reference,type 将表示被引用的类型。如:remove_references<int&>,其 type 为 int 。

  • 给定一个迭代器 beg
  • remove_reference<decltype (*beg) >::type 将获得 beg 引用的元素的类型: decltype(*beg) 返回元素类型的引用类型。
  • remove_reference::type 除去引用,剩下元素类型本身。

​  组合使用 remove_reference、 尾置返回类型及 decltype 关键字,我们就可以在函数中返回元素值的拷贝

template <typename It>
auto fcn(It beg,It end) -> remove_reference<decltype(*beg)>::type {
    // 处理序列
    return *beg;	// 返回序列中一个元素的拷贝
}

​  注意, type 是一个类的类型成员, 而该类依赖于模板的一个类型参数(即typedef T type;),所以 type 是一个依赖名。 因此, 我们 必须在返回类型的声明中使用 typename 来告知编译器, type 表示一个类型

​  每个类型转换模板的工作方式都与 remove_reference 类似——每个类型转换模板都有一个名为 type 的 public 成员,它表示一个类型。此类型与模板自身的模板类型参数相关,其关系如模板名所示。如果不可能(或者没必要)转换模板实参,则 type 成员就是模板实参类型本身。
​  例如,如果 T 是一个指针类型,则 remove_pointer<T>::type 是 T 指向的类型。如果 T 不是一个指针,则无须进行任何转换,从而 type 具有与 T 相同的类型。

16.2.4 函数指针和实参推断

​  当我们直接用一个函数模板名 初始化一个函数指针为一个函数指针赋值 时,编译器使用函数指针的类型来推断模板实参(对于函数指针来说,其类型还包括形参类型以及返回类型)

​  ​例如,假定我们有一个函数指针,它指向的函数返回 int,接受两个实参,每个参数都是指向 const int 的引用。我们可以使用该指针指向 compare 的一个实例:

template <typename T> int compare(const T&,const T&);
// 实例化 int compare(const int&,const int&) 并由 pf1 指向它
int (*pf1)(const int&,const int&) = compare;

​  pf1 中参数的类型决定了 T 的模板实参的类型。在这里,T 的模板实参类型为 int。指针 pf1 指向 compare 的 int 版本实例。如果不能从函数指针的类型确定模板实参,则产生错误。典型例子如下:

// !!!!!!!!!!!!!!!!!!!!!!!!!!!!重点
void func(int(*)(const string&,const string&));
void func(int(*)(const int&,const int&));
func(compare);			// 错误,使用 compare 的哪个实例?

​  在这里, func 能接受 string 与 int 的 compare 实例,所以无法确定 func 的实参的唯一实例化版本,此调用失败。

​​  我们可以通过指定显式模板实参来消除这个歧义

func(compare<int>);		// 传递 compare(const int&,const int&);

Note:当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值

16.2.5 模板实参推断和引用

​​  为了理解如何从函数调用进行类型推断,考虑下面例子:

template<typename T> void f(T &p);

其中函数参数 p 是一个模板类型参数 T 的引用,对于函数形参是一个指向模板类型参数的引用,需要注意两点

  • 编译器会应用正常的引用绑定规则(比如右值引用不能绑定左值);
  • 引用中没有 top-const 与 low-const 之分,如果一定要称呼,那引用只有底层 const 与非 const 之分,因为不能通过 const 引用修改其引用的对象。

从左值引用函数参数推断类型(1)

​​  当一个函数参数是模板类型参数的一个普通(左值)引用时(即,形如 T& ),普通的引用绑定规则告诉我们,只能传递给它一个左值。实参可以是 const 类型,也可以不是。如果实参是 const 的,则 T 被推断为 const 类型

template <typename T> void f1(T&);	// 实参必须是一个左值
f1(i);		// i 是一个 int,则 T 是 int
f1(ci);		// ci 是 const int,模板参数 T 是 const int
f1(5);		// 错误,传递的实参必须是一个左值

​​  此外,我们也可以传给它一个右值引用,这将在(引用折叠)中详细讲解。

​​  ​如果函数参数类型是模板类型参数的一个 const 普通(左值)引用时(即,形如 const T& ),我们可以传递给它任何类型的实参——一个对象( const 或 非 const )、右值(字面值、临时对象等)。当函数参数本身是 const 引用时,T 的类型推断的结果不会是一个 const 类型。因为 const 已经是函数参数类型的一部分;因此,它不会也是模板类型参数的一部分

template <typename T> void f2(const T&);	// 可以接受一个右值
// f2 中的参数是 const&;实参中的 const 是无关的
// 下面的调用中,**f2** 的函数参数都被推断为 const int&
f2(i);		// i 是 int,T 是 int
f2(ci);		// ci 是 const int,但 T 是 int
f2(5);		// T 是 int,const T& 可以绑定一个右值

从右值引用函数参数推断类型(1)

​​  当一个函数参数是模板类型参数的一个右值引用时(即,形如 T&& ),我们可以传给它一个右值。若传递给它一个右值,类型推断过程类似左值引用函数参数的推断过程。推断出的 T 的类型是该右值实参的类型

template <typename T> void f3(T&&);
f3(42);			// T 是 int

​​  此外,我们也可以传给它一个左值,这将在下一小节(引用折叠)中详细讲解。

引用折叠(又称引用坍缩,reference collapsing)和右值引用参数——从左(右)值引用函数参数推断类型(2)

​​  假定 i 是 int 对象,我们可能认为 f3(i) 这样是不合法的,毕竟 i 是一个左值,通常我们不能把一个左值绑定到右值引用但是 C++ 在正常绑定规则之外定义了两个例外规则,从而在特定情况下允许这种绑定这两个例外规则是标准库函数 move 正确工作的基础

​​​  第一个例外绑定规则影响右值引用参数的推断如何进行。如上述情况,当我们调用 f3(i) 时,编译器推断 T 为 int& ,而非 int 。 即 ,当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型为实参的左值引用类型

​​   T 被推断为 int&,看起来好像 f3 的函数参数应该是一个类型 int& 的右值引用。一般情况下,我们不能(直接)定义一个引用的引用。但是通过 类型别名模板类型参数 间接定义是可以的

​​  在这种情况下,我们有 第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下,引用会折叠成一个普通的左值引用类型。在 C++11 中,有一个例外:右值引用的右值引用为折叠成右值引用

即,对于一个给定类型 X :

  • 即:X& &, X& && 和 X&& & 都会折叠为 X& ;
  • 而 X&& && 折叠成 X&&

Note:引用折叠只能适用于间接创建的引用的引用,比如类型别名或模板类型参数

编写接受右值引用参数的模板类型

​​  模板参数可以推断为一个引用类型,这一特性对模板内的代码可能有令人惊讶的影响

template <typename T> void f3(T&& val) {
    T t = val;		// 拷贝还是绑定一个引用?
    t = fcn(t);		// 赋值只改变 t 还是既改变 t 又改变 val
    if(val == t) { /* */ }		// 若 T 是引用类型,则一直为 true
}

​​当我们对一个右值调用 f3 的时候,例如字面常量 42,T 为 int。在此情况下,局部变量 t 的类型为 int,通过拷贝参数 val 的值被初始化。当我们对 t 赋值时,参数 val 保持不变。
​​​  另一方面,当我们对一个左值 i 调用 f3 时,则 T 为 int&。因此 t 的初始化被绑定到 val,改变 t 将改变 val。if 判断永远都是 true。

​​​  当代码中涉及的类型可能是普通(非引用)类型,也可能是引用类型时,编写正确的代码就变得异常困难(虽然 remove_reference 这样的类型转换类可能会有帮助)。

​​​  在实际中,在函数模板中使用右值引用通常会用于两种情况:模板转发其实参模板被重载

​​​  目前应该注意的是,使用右值引用的函数模板通常使用我们在 13.6.3 节中看到的方式进行重载

template<typename T> void f(T&&);		// 绑定到非 const 右值
template<typename T> void f(const T&);	// 绑定到左值和 const 右值

与非模板函数一样,第一个版本将绑定到可修改(即非 const )的右值,此版本对于非 const 的右值是精确匹配(也是更好的匹配)。

在这里插入图片描述

16.2.6 理解 std::move

​​  标准库函数 std::move 是使用右值引用的函数模板的一个很好的例子。
​​  我们知道,虽然不能直接将一个右值引用和一个左值绑定在一起,通过 move 可以获得一个绑定到左值上的右值引用。由于 move 本质上可以接受任何类型的实参,因此我们也可以猜到它是一个函数模板。

std::move 是如何定义的

​​  标准库是这样定义 move 的

// 在返回类型和类型转换中也要用到 typename 
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {	// 指出 remove_reference<T>::type 是一个类型
    return static_cast<typename remove_reference<T>::type&&>(t);
}

static_cast 是关键字。
​​  返回类型为对象的右值引用的函数调用是将亡值表达式,可以被绑定到右值引用上
在这里插入图片描述

​​  我们可以发现,函数形参是 T&&,所以我们可以传递给 move 左值(由于引用折叠)或者右值:

string s1("hi!"), s2;
s2 = std::move(string("bye!"));	// ok,从一个右值移动数据
s2 = std::move(s1);				// ok,但是在赋值后,s1 的值是不确定的

std::move 是如何工作的

​​  在第一个赋值中,传递给 move 的实参是 string 的构造函数的右值结果—— string("bye!") 。如我们所见过的,当向一个右值引用函数传递一个右值时,由实参推断出的类型为被引用类型(参见 16.2.5 节)。因此,在 std::move(string(“bye!”)) 中:

  • 推断出的 T 的类型为 string
  • 因此,remove_reference 用 string 进行实例化
  • remove_reference<string> 的 type 成员是 string
  • move 的返回类型是 string&&
  • move 的函数形参 t 的类型为 string&&

​​  这个调用实例化 move<string> ,即函数:string&& move(string &&t),函数返回 static_cast<string&&>(t),实际并没有发生类型转换。因此,此调用结果返回它所接受的右值引用。

​​  ​考虑第二个赋值,传递给 move 的实参是一个左值:

  • 推断出 T 的类型为 string&
  • remove_reference 用 string& 实例化
  • remove_reference<string&> 的 type 成员是 string
  • move 仍然返回 string&&
  • move 的函数形参 t 实例化为 string& &&,折叠为 string&。

​​  因此,这个调用实例化 move<string&>,即:string&& move(string &t),通过类型转换,得到 string&&。

从一个左值 static_cast 到一个右值引用是允许的

​​  通常情况下, static_cast 关键字只能用于其他合法的类型转换。但是有一条针对右值的特许规则:虽然不能隐式地将一个左值转换为右值引用,但我们可以用 static_cast 显式地将一个左值转换为一个右值引用。但是要注意,右值引用并不是右值。
​​  对于操作右值引用的代码来说, 将一个右值引用绑定到一个左值的特性(之前提到的两个引用绑定的例外规则)允许它们截断左值有时候,我们知道截断一个左值是安全的, 例如在我们的 StrVec 类的 reallocate 函数(参见 13.6.1 节)中。

  • 一方面, 通过允许进行这样的转换, C++ 语言认可了这种用法。
  • 另一方面, 通过强制使用 static_cast, C++ 语言试图阻止我们意外地进行这种转换。

​​  最后, 虽然我们可以直接编写这种类型转换代码, 但使用标准库 move 函数是容易得多的方式。 而且, 统一使用 std::move 使得我们在程序中查找潜在的截断左值的代码变得很容易。

16.2.7 转发

​​  某些函数需要将其一个或多个实参连同类型不变地转发给其他函数在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是 const 的以及实参是左值还是右值

​​​  作为一个例子,我们将编写一个函数,它接受一个可调用表达式和两个额外实参。我们的函数将调用给定的可调用对象,将两个额外参数逆序传递给它。下面是我们的翻转函数初步模样:

// flip1 是一个不完整的实现,顶层 const 和引用丢失了
template <typename F,typename T1,typename T2>
void flip1(F f, T1 t1, T2 t2) {
    f(t2, t1);
}

​​  这个函数一般情况下没有问题。但当我们用它调用一个接受引用参数的函数时就会出现问题:

void f(int v1,int &v2) {
    cout << v1 << " " << ++ v2 << endl;
}

​​  在这段代码中 f 改变了绑定到 v2 的实参的值。 但是, 如果我们通过 flip1 调用 f , f 所做的改变就不会影响实参。

f(42, i); 			// f 改变了实参 i
flipl (f, j, 42); 	// 通过 flipl 调用 f 不会改变 j

​​  问题在于 flip1 传递给 f 的参数 t1 。此参数类型是一个普通的、非引用的类型 int ,而非 int& 。因此,这个 flip1 调用被实例化为:

void flip1(void(*fcn)(int, int & ), int t1, int t2);

j 被拷贝到 flip1 函数参数 t1 中。 f 中的应用参数被绑定到函数形参 t1 ,而非原始实参 j 。

定义能保持类型信息的函数参数

​​  为了通过翻转函数传递一个引用,我们需要重写函数,使其参数能保持给定实参的“左值性”。更进一步,我们也希望保持参数的 const 属性。

​​  通过将一个函数参数定义为一个指向模板类型参数的右值引用(即 T&& t),我们可以保持其对应实参的所有类型信息
​​  而使用引用参数(无论是左值还是右值)使得我们可以保持实参的 const 属性,因为在引用类型中的 const 本质上是底层的,不会被忽略,而会被编译器推断出来作为模板类型参数的一部分。
​​  如果将函数参数定义为指向模板类型参数的右值引用通过引用折叠,就可以保持实参的左值/右值属性左值实参对应左值引用,右值实参对应右值引用)。

template <typename F,typename T1,typename T2>
void flip2(F f, T1&& t1, T2&& t2) {
    f(t2, t1);
}

​​对于 flip2 ,当我们这样调用时 flip2(f, j, 42),j 的值将会发生改变,因为 j 是一个左值,我们将其绑定到右值引用时,T1 会被推断为 int&,引用折叠后 t1 也就是 int&,所以 t1 会被绑定到 j。

Note:如果一个函数参数是指向模板类型参数的右值引用(如 T&& t),它对应的实参的 const 属性和左值/右值属性将得到保持。

​​​  flip2 对接受左值引用的函数工作没有问题,但不能用于接受右值引用参数的函数,例如:

void g(int &&i, int &j) {
    cout << i << " " << j << endl;
}

​​  如果我们试图通过 flip2 调用 g (无论传递给 flip2 的是左值还是右值),则参数 t2 将被传递给 g 的右值引用参数,即使我们传递一个右值给 flip2 :

flip2(g,i,42);	//错误:不能用一个左值来初始化 int&& 

​​  上述代码会出现:不能从一个左值实例化 int&& 的错误(!!注意:类型为右值引用的变量,它还是一个变量,而变量是左值)函数参数和其它任何变量一样,都是左值表达式
​​  要想通过 flip2 调用接受右值引用参数的函数 g,则必须满足g 的函数实参是将亡值或纯右值。而要想保持实参的所有类型信息,则 g 的函数实参是对应的引用类型(比如左值引用)
  下面将要介绍的标准库 std::forward 将满足以上两点。

在调用中使用 std::forward 保持类型信息

​​  我们可以使用一个名为 forward 的新标准库设施来传递 flip2 的参数,它能保持原始实参的类型。类似 move , forward 定义在头文件 utility 中。与 move 不同, forward 必须通过显式模板实参来调用forward 返回类型是该显式实参类型的右值引用。即, forward<T> 的返回类型是 T&&
​​  ​通常情况下,我们使用 forward 传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward 可以保持给定实参的左值/右值属性
​​  ​而且 std::forward<T>(arg) 可以被绑定到右值引用上,因为它作为一个返回类型为对象的右值引用的函数调用表达式,是一个将亡值

template <typename Type>void intermediary(Type&& arg) {
    finalFcn(std::forward<Type>(arg));
    // ...
}

本例中我们使用 Type 作为 forward 的显式模板实参类型,它是从 arg 推断出来的。由于 arg 是一个模板类型参数的右值引用, Type 将表示传递给 arg 的实参的所有类型信息。

  • 如果实参是一个右值,则 Type 是一个普通( 非引用) 类型, std::forward 将返回 Type&& 。
  • 如果实参是一个左值, 则由于第一个例外绑定规则, Type 是一个左值引用类型。 在此情况下, 返回类型是一个指向左值引用类型的右值引用。 再次对 std::forward 的返回类型会进行引用折叠, 将返回一个左值引用类型,即 Type& 。

Note:当用于一个指向模板参数类型的右值引用函数参数(T&&)时, forward 会保持实参类型的所有细节

​​  ​使用 forward ,我们可以再次重写翻转函数:

template <typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2) {
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

​​  如果我们调用 flip(g, i, 42),i 将以 int& 类型传递给 g,42 将以 int&& 类型传递给 g。

与 std::move 相同,对 std::forward 不使用 using 声明是一个好主意


16.3 重载与模板

​​  函数模板可以被另一个模板函数普通非模板函数重载。跟普通函数重载一样,名字相同的函数必须具有不同数量或类型的参数。

​​  如果涉及到函数模板,则函数匹配规则会在下面几个方面受到影响

  • 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例
  • 候选的函数模板的实例总是可行的,因为模板实参推断会排除任何不可行的模板。
  • 与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可用于函数模板调用的类型转换非常有限,只有 const 转换、数组或函数指针转换(参见 16.2.1 节)。
  • 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则
    —— 如果同样好的函数中只有一个是非模板函数,则选择此函数。
    —— 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
    —— 否则,则此调用有歧义

前两点是互补的,候选函数同名非模板的函数(不一定可行) + 同名的可行的函数模板实例(相比普通非模板函数已经经历了可行函数的检测,不然不会实例化)

编写重载模板

​​  作为一个例子,我们将构造一组函数,它们在调试中可能很有用。我们将这些调试函数命名为 debug_rep ,每个函数都返回一个给定对象的 string 表示。我们首先编写此函数的最通用版本,将它定义为一个模板,接受一个 const 对象的引用:

// 打印任何我们不能处理的类型: 该对象可以是任意具有输出运算符的类型
template<typename T> string debug_rep(const T &t) {
    ostringstream ret;
    ret << t;
    return ret.str();	// 返回 ret 绑定的 string 的一个副本
}

此函数可以用来生成一个对象对应的 string 表示,该对象可以是任意具有输出运算符的类型。
​​  接下来我们将定义打印指针的 debug_rep 版本:

// 注意:此函数不能用于char*对象
template <typename T> string debug_rep(T *p) {
    ostringstream ret;
    ret << "pointer: " << p;      // 打印指针本身的值
    if (p) {
        ret << " " << debug_rep(*p);  // 打印p指向的值
    } else {
        ret << " null pointer";   // 或者指出指针p为空
    }
    return ret.str();
}

注意此函数不能用于打印字符指针因为IO库为 char* 值定义了一个 << 版本, 此版本假定 char 指针表示一个空字符结尾的字符数组,并打印数组的内容而非地址值。所以如果 p 是 char* ,那么 ret << "pointer: " << p; 将不会打印 p 本身的值。

​​  我们可以这样使用这些函数:

string s("hi");
cout << debug_rep(s) << endl;

对于这个调用,只有第一个版本的 debug_rep 是可行的。第二个 debug_rep 版本要求一个指针参数,但在此调用中我们传递的是一个非指针对象。因此编译器无法从一个非指针实参实例化一个期望指针类型参数的函数模板,因此实参推断失败。由于只有一个可行函数,所以此函数被调用。

​​  如果我们用一个指针调用 debug_rep :

cout << debug_rep(&s) << endl;

两个函数都能生成可行的实例:

  • debug_rep(const string&) ,由第一个版本的 debug_rep 实例化而来, T 被绑定到 string 。
  • debug_rep(string*) ,由第二个版本的 debug_rep 实例化而来, T 被绑定到 string 。

第二个版本的 debug_rep 实例是此调用的精确匹配。第一个版本的实例需要进行普通指针(string)到 const 指针(const string*)的转换。正常函数匹配规则告诉我们应当选择第二个模板,实际上编译器确实选择了这个版本。

多个可行的函数模板实例

​​  作为另外一个例子,考虑以下调用:

const string* sp = &s;
cout << debug_rep(sp) << endl;

​​此例中的两个函数模板实例都是可行的,且都是精确匹配:

  • debug_rep(const string*&) ,由第一个版本的 debug_rep 实例化而来, T 推断为 string* 。
  • debug_rep(const string*) ,由第二个版本的 debug_rep 实例化而来, T 推断为 const string 。

此时,正常函数匹配规则无法区分这两个实例,我们可能会觉得这个调用有二义性。但,根据重载函数模板的特殊规则,此调用被解析为 debug_rep(T*) ,即,函数模板中更特例化的版本
​​  设计这条规则的原因:没有它,将无法对一个 const 指针调用指针版本的函数 debug_rep 。问题在于模板 debug_rep(const T&) 本质上可以用于任何类型,包括指针类型。该模板比 debug_rep(T*) 更通用,后者只能用于指针类型,也就是说后者更加特例化。如果没有这条规则,传递给 const 的指针的调用永远是有歧义的。

非模板和模板的重载

​​  作为下一个例子,我们将定义一个普通非模板版本的 debug_rep 函数来打印双引号包围的 string :

string debug_rep(const string& s)
{
    return '"' + s + '"';
}

此时,同样有2个可行函数:

  • debug_rep(const string& ),第一个模板, T 推断为 string 。
  • debug_rep(const string&) ,普通非模板函数。

此时,虽然两个函数具有相同参数列表,显然两者提供同样好的匹配。但是,编译器会选择非模板版本。因为有多个同样好的函数模板实例时,编译器会选择最特例化的版本,处于相同的原因,一个非模板函数比一个函数模板更特例化。

Note:对于一个调用,如果一个非函数模板与一个函数模板提供同样的匹配,则选择非模板版本。因为前者更特例化。

重载模板和类型转换

​​  有一种情况还没讨论到:C风格字符串指针和字符串字面常量现在还有一个接受 string 的 debug_rep 版本,我们可能期望一个传递字符串的调用会匹配这个版本。但是考虑下面这个调用:

cout << debug_rep("hi world!") << endl; // 调用debug_rep(T*)

本例中,所有三个 debug_rep 版本都是可行的:

  • debug_rep(const T&) ,T 推断为 char[10]。
  • debug_rep(T*) ,T 推断为 const char。
  • debug_rep(const string&) ,要求发生从 const char* 到 string 的类型转换。

对给定实参来说,两个模板都提供精确匹配——第二个模板要进行一次(许可的)数组类型到指针类型的转换,而对于函数匹配来说,这种转换被认为是精确匹配(参见 6.6.1 节)。非模板版本是可行的,但需要进行一次用户定义的类型转换,因此它没有精确匹配的优先极高,所以有两个模板成为可能调用的。和之前一样,编译器会选择第二个版本实例,因为 T* 版本更加特例化。

​​  如果希望将字符串按 string 处理,可以定义另外两个非模板重载版本:

// 将C字符串指针转换为string,并调用string版本的debug_rep(转交给另外一个版本的debug_rep处理)
string debug_rep(char* p)
{
  return debug_rep(string(p));
}
string debug_rep(const char* p)
{
  return debug_rep(string(p));
}

缺少声明可能导致程序行为异常

​​  值得注意的是,为了使 char* 版本的 debug_rep 正确工作,在定义此版本时, debug_rep(const string&) 的声明必须在作用域中;否则,可能调用错误的 debug_rep 版本:

template <typename T> string debug_rep(const T& t);
template <typename T> string debug_rep(T* p);
// 为使debug_rep(char*)定义正确的构造,下面的声明必须在作用域中
string debug_rep(const string& s);
string debug_rep(char* p)
{
    return debug_rep(string(p));
}

​​  通常,如果使用了一个忘记声明的函数,代码将编译失败。但对于具有重载函数模板的函数实例而言,则不是这样。如果编译器可以从模板实例化出与调用匹配的版本,则缺少的声明就不重要了。当然这也会导致程序行为异常,且难发现。在本例中,如果忘记声明非模板的接受一个 string 的 debug_rep 版本,编译器就会默默地实例化接受 const string& 的模板版本。


16.4 可变参数模板(variadic template)

​​  一个可变参数模板(variadic template)就是接受一个可变数目参数的模板(函数或类)。可变数目的参数被称为参数包(parameter packer)。存在两种参数包:模板参数包(template parameter packer),表示零个或多个模板参数;函数参数包(function parameter packer),表示零个或多个函数参数。

​​​我们用一个省略号(...)来指出一个模板参数或函数参数表示一个参数包

  • 在一个模板参数列表中,class…typename… 指出接下来的参数表示零个或多个类型参数的列表一个类型名后面跟一个省略号(...)表示零个或多个给定类型的非类型参数的列表
  • 在函数参数列表中,如果一个函数参数的类型是一个模板参数包,则此参数也是一个函数参数包
// Args 是一个模板参数包;rest 是一个函数参数包
// Args 表示零个或多个模板类型参数
// rest 表示零个或多个函数参数
template <typename T,typename... Args>
void foo(const T&t, const Args&... rest);

​​声明了 foo 是一个可变参数函数模板,它有一个名为 T 的类型参数,和一个名为 Args 的模板参数包。这个包表示零个或多个额外的类型参数。foo 的函数参数列表包含一个 const & 类型的参数,指向 T 的类型,还包含一个名为 rest 的函数参数包,此包表示零个或多个函数参数。

​​  与往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断(模板/函数)包中的参数数目。例如,给定下面调用:

int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i,s,42,d);		// 函数包和模板包中都有三个参数
foo(s,42,"hi");		// 函数包和模板包中都有两个参数
foo(d,s);			// 函数包和模板包中都有一个参数
foo("hi");			// 函数包和模板包都是空包

​编译器会为 foo 实例化出四个不同的版本:

void foo(const int&,const string&,const int&,const double&);
void foo(const string&,const int&,const char[3]&);
void foo(const double&,const string&);
void foo(const char[3]&);

​​在每个实例中,T 的类型都是从第一个实参的类型推断出来的。剩下的实参(如果有)提供函数额外实参的数目和类型

sizeof… 运算符

​​  当我们需要知道包中有多少元素时,可以用 sizeof… 运算符。sizeof… 也返回一个常量表达式,而且不会对其实参求值

template <typename T,typename... Args> void f(Args ...args) {
    cout << sizeof...(Args) << endl;		// 类型参数的数目
    cout << sizeof...(args) << endl;		// 函数参数的数目
}

16.4.1 编写可变参数函数模板

​​  在6.2.6节中我们知道,可以用 initializer_list 来定义一个可接受可变数目实参的函数。但是,所有实参必须具有相同的类型(或它们的类型都可以转换为某一个类型)

​​  ​所以,当我们既不知道想要处理的实参的数目也不知道它们的类型时,可变参数函数(使用了模板参数包和函数参数包的函数)是很有用的。作为一个例子,我们将定义一个名为 print 的函数,它在一个给定流上打印给定实参列表的内容。

​​  可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。我们的 print 函数也是如此。为了终止递归,我们还需要定义一个非可变参数的 print 函数,它接受一个流和一个对象:

template <typename T>
ostream& print(ostream &os, const T &t) {
    return os << t;		// 包中最后一个元素之后不打印分隔符
}
// 包中出了最后一个元素之外的其他元素都会调用这个版本的 print
template <typename T, typename... Args>
ostream& print(ostream &os, const T &t, const Args& ...rest) {
    os << t << ",";		// 打印第一个实参
    return print(os, rest...);		// 递归调用,打印其他实参
}

第一个版本的 print 负责终止递归并打印初始调用的最后一个实参,第二个版本的 print 是可变参数版本,它打印绑定到 t 的实参,并调用自身来打印函数参数包中的剩余值。

​​  ​这段程序的关键部分是可变参数函数中对自身的递归调用

return print(os, rest...);

​​  我们可以发现,可变参数版本的 print 有三个参数,os,const T& 与 一个参数包。而此调用只传递了两个实参。其结果是 rest 中的第一个实参被绑定到 t,剩余实参形成下一个 print 调用的参数包。当此包中只剩下一个参数时,虽然两个版本的 print 都能够精确匹配,但是非函数模板优先于函数模板,所以最后一个 print 调用的非函数模板的 print

当定义可变参数版本的 print 时,非可变参数版本的声明必须在作用域中。否则,可变参数版本可能会无限递归(即一直调用函数模板的实例)

16.4.2 包扩展(packer expand)

​​  对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展(expand)。当扩展一个包时,我们还要提供用于每个扩展元素的模式(pattern)扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(…) 来触发扩展操作

​​  ​例如,我们的 print 函数包含两个扩展:

template <typename T,typename... Args>
ostream& print(ostream &os, const T &t, const Args&... rest) {	// 扩展 Args 为 const &
    os << t << ",";
    return print(os, rest...);		// 扩展 rest
}

第一个扩展操作扩展模板参数包,为 print 生成函数参数列表。第二个扩展操作出现在对 print 的调用中。此模式为 print 调用生成实参列表。
​​  在对模板参数包 Args 的拓展中编译器将模式 const Args& 应用到模板参数包 Args 中的每个元素。因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形如 const type& (type 指的是模板参数包中每个元素的类型。例如:

print(cout, i, s, 42);	// 函数参数包和模板参数包中有两个参数

最后两个实参的类型和模式一起确定了右端参数的类型。此调用被实例化为:

ostream&
print(ostream&, const int&, const string&, const int&);

​​  第二个扩展发生在对 print 的(递归)调用中。在此情况下,模式是函数参数包的名字(即 rest)。此模式扩展出一个由包中元素组成的、逗号分隔的列表。因此,这个调用等价于:

print(os, s, 42);

理解包扩展——即debug_rep(rest)…与debug_rep(rest…)的区别

​​  print 中的函数参数包扩展仅仅将包扩展为其构成元素(即 rest…),C++还允许更为复杂的扩展模式。例如,我们可以编写第二个可变参数函数,对其每个实参调用 debug_rep,然后调用 print 打印结果 string:

template <typename... Args>
ostream& errorMsg(ostream &os, const Args&...rest) {
    // print(os, debug_rep(a1), debug_rep(a2), ... , debug_rep(an));
    return print(os, debug_rep(rest)...);
}

​​​  这个 print 调用 print(os, debug_rep(rest)...) 使用模式 debug_reg(rest)。此模式表示我们希望对函数参数包 rest 中的每个元素调用 debug_rep。扩展结果将是一个逗号分隔的 debug_rep 调用列表。即,对于下面调用:

errorMsg(cerr, fcnName, code.num(), otherDate, "other", item);

最后一条 print 调用​​就好像我们这样编写代码一样:

print(cerr, debug_rep(fcnName), debug_rep(code.num()), 
			debug_rep(otherData), debug_rep("other"), 
			debug_rep(item));

​​与之相对,下面的模式会编译失败

// 将包传递给 debug_rep; print(os,debug_rep(a1,a2,...,an))
print(os, debug_rep(rest...));		// 错误,此调用无匹配函数

​​这段代码的问题是我们在 debug_rep 调用中扩展了 rest,它等价于:

print(cerr, debug_rep(fcnName, code.num(), otherData, "other", item));

​​在这个扩展中,我们试图用一个具有五个实参的列表来调用 debug_rep,但并不存在与此调用匹配的 debug_rep 版本。debug_rep 函数不是可变参数,去没有哪个 debug_rep 版本接受五个参数。

Note:扩展中的模式会独立地应用于包中的每个元素

16.4.3 转发参数包

​​  在有了C++11后,我们可以组合使用可变参数模板与 forward 机制来编写函数,实现将其可变实参(所有类型信息)不变地传递给其他函数。作为例子,我们将为 StrVec 类添加一个 emplace_back 成员。标准库容器的 emplace_back 成员是一个可变参数成员模板,它用其实参管理的内存空间中直接构造一个元素。
​​  ​我们为 StrVec 设计的 emplace_back 版本也应该是可变参数的,因为 string 有多个构造函数,参数各不相同。由于我们希望能使用 string 的移动构造函数,因此还需要保存传递给 emplace_back 的实参的所有类型信息
​​  ​如我们所见,保持实参的类型信息是一个两阶段的过程首先,为了保持实参中的类型信息,必须将 emplace_back 的函数参数定义为模板类型参数的右值引用

class StrVec {
public:
    template <class... Args> void emplace_back(Args&&...);
    // 其他成员这里省略
};

​​模板参数包扩展中的模式是 &&,意味着每个函数参数将是一个指向其对应实参的右值引用。
​​​  其次,当 emplace_back 将这些实参传递给 construct 时,我们必须使用 forward 来保持实参的原始类型

template <class... Args>
inline void StrVec::emplace_back(Args&& ...args) {
    chk_n_alloc();		// 如果需要的话重新分配 StrVec 的内存空间
    alloc.construct(first_free++,std::forward<Args>(args)...);
    // construct本身也是构造,与 emplace_back 类似
}

​​emplace_back 的函数体调用了 chk_n_alloc (参见13.5节)来确保有足够空间容纳一个新元素,然后调用了 construct 在 first_free 指向的位置中创建一个元素。construct 调用中的扩展为:

std::forward<Args>(args)

​​既扩展了模板参数包 Args,也扩展了函数参数包 args。此模式生成如下形式的元素

std::forward<Ti>(ti)

​​其中 Ti 表示模板参数包中第 i 个元素的类型,ti 表示函数参数包中第 i 个元素。例如,假定 svec 是一个 StrVec,如果我们调用:

svec.emplace_back(10,'c');

​​construct 调用中的模式会扩展出:

std::forward<int>(10), std::forward<char>(c)

​​  ​通过在此调用中使用 forward,我们保证如果用一个右值引用调用 emplace_back,则 construct 也会得到一个右值 (这是 forward 可以保证的)。


16.5 模板特例化

​​  编写单一模板,使之对任何可能的模板实参都是最合适的,都能实例化,这并不总是能办到。在某些情况下,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或做得不正确。其他时候,我们也可以利用某些特定知识来编写更高效的代码,而不是从通用模板实例化。当我们不能 (或不希望) 使用模板版本时,可以定义类或函数模板的一个特例化版本。
​​  ​我们的 compar 函数是一个很好的例子,它展示了函数模板的通用定义不适合一个特定类型(即字符指针)的情况。我们希望 compare 通过调用 strcmp 来比较两个字符指针,而并非比较指针值。实际上,我们已经重载了 compare 函数来处理字符串的字面常量:

//第一个版本:通用版本,可以比较任意两个类型
template <typename T> int compare(const T&, const T&);
//第二个版本:处理字符串字面常量
template <size_t N, size_t M> int compare(const char(&)[N], const char(&)[M]);

但是,只有当我们传递给 compare 一个字符串字面常量或一个字符数组时,编译器才会调用接受两个非类型模板参数的第二个版本的 compare。如果我们传递给它字符指针,就会调用第一个版本:

const char *p1 = "hi", *p2 = "hhh";
compare(p1, p2);		// 调用第一个版本
compare("hi", "hhh");	// 调用第二个版本

我们无法将一个指针转换为一个数组的引用,因此参数是 p1 和 p2 时,会调用第一个版本的 compare,第二个版本的 compare 是不可行函数。

​​​  为了处理字符指针 (而不是数组),可以为第一个版本的 compare 定义一个 模板特例化(template specialization) 版本。一个特例化版本就是模板的一个独立定义,在其中一个或多个模板参数指定为特定的类型或值

关于显式(全)特例化要注意的是

  • 显式特例化必须出现在非特例化的模板声明后(即原模板的声明必须在作用域中);
  • 显式特例化必须在第一条导致相同实例被隐式实例化的语句之前(即在任何使用该模板实例的代码之前,特例化版本的声明也必须在作用域中);
  • 仅声明但未定义的模板特例化,可以像其他不完整类型一样使用(例如在类定义中定义指向本类的指针或引用)
// 1.显式特例化必须出现在非特例化的模板声明后
namespace N {
    template<class T> class X { /*...*/ }; // 主模板
    template<> class X<int> { /*...*/ }; // 同命名空间中的特化
 
    template<class T> class Y { /*...*/ }; // 主模板
    template<> class Y<double>; // 对 double 特化的前置声明
}
template<>
class N::Y<double> { /*...*/ }; // OK:同命名空间中的特化

// 2.显式特例化必须在第一条导致隐式实例化的语句之前
class String {};
template<class T> class Array { /*...*/ };
template<class T> void sort(Array<T>& v) { /*...*/ } // 主模板
 
void f(Array<String>& v) {
    sort(v); // 隐式实例化 sort(Array<String>&), 
}            // 使用初等模板 sort()
template<>  // 错误:sort(Array<String>) 的显式特化出现在隐式实例化之后
void sort<String>(Array<String>& v);

// 3.
template<class T> class X; // 主模板
template<> class X<int>; // 特化(声明,不定义)
X<int>* p; // OK:指向不完整类型的指针
X<int> x; // 错误:不完整类型的对象

定义函数模板特例化——只能使用显式(全)特例化

​​  当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应该使用关键字 template 后跟一个空尖括号对(<>)。空尖括号指出我们将为原模板的所有模板参数提供实参

// compare 的特殊版本,处理字符指针
template <> int compare (const char* const &p1, const char* const &p2) {
    return strcmp(p1, p2);
}

​​  需要注意的是:当我们定义一个特例化版本时,函数参数类型必须与一个先前声明的非特例化的模板中对应的类型匹配。本例中特例化的原模板如下:

template <typename T> int compare(const T&, const T&);

​​  所以,T 为 const char*,我们的函数要求一个指向此类型 const 版本的引用。一个指针类型的 const 版本是一个常量指针而不是指向 const 类型的指针,所以在特例化版本中,函数参数的类型为 const char* const &。

函数重载与模板特例化

​​  当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即我们为原模板的一个特殊实例提供了定义。要清楚一点:一个特例化版本本质上是一个实例,而非函数名的一个重载版本

Note:模板特例化的本质是用户代替编译器显式地实例化一个模板,而非重载它。因此,特例化不影响函数匹配

​​  我们选择将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数,这将会影响到函数匹配

​​  例如,我们已经定义了两个版本的 compare 函数模板,一个接受数组引用参数,另一个接受 const T&。我们还定义了一个特例化版本来处理字符指针,这对函数匹配没有影响。当我们对字符串的字面值常量调用 compare 时:

compare("hi", "mom");

对此调用,两个函数模板都是可行的,且提供同样好的(即精确)匹配。但是,接受字符数组参数的版本更特例化,因此编译器会选择它。
​​  如果我们将接受字符指针的 compare 版本定义为一个普通的非模板函数(而不是模板的一个特例化版本),此调用的解析就会不同。在此情况下,将产生三个可行函数:接受字符指针版本的两个函数模板实例和非模板的函数。所有三个函数都提供同样好的匹配。如前所述,编译器会选择非模板版本函数。

关键概念:普通作用域规则应用于模板特例化

为了特例化一个模板

  • 原模板的声明必须在作用域中
  • 而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中

​​  对于普通类和函数,丢失声明的情况(通常)很容易被发现——因为这会导致编译器不能继续处理代码。但是,如果丢失了一个特例化版本的声明,编译器通常会实例化原模板,很容易产生模板及其特例化版本声明顺序导致的错误,而这种错误又很难查找
​​  如果一个程序使用一个特例化版本,而同时原模板的一个实例具有相同的模板实参集合,就会产生错误。但是这种错误编译器又无法发现。

Best Practices:模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。

类模板特例化

​​  类模板也是可以显式(全)特例化的

​​  作为一个例子,我们将为标准库 hash 模板定义一个特例化版本,可以用它将用户自定义对象保存在无序容器中。因为默认情况下,无序容器使用 hash<key_type> 来组织其元素。为了让我们自己的数据类型也能使用这种默认的组织方式,必须定义 hash 模板的一个特例化版本。一个特例化 hash 类必须定义:

  • 一个重载的调用运算符,它接受一个容器关键字类型的对象,返回一个 size_t
  • 两个类型成员,result_type 和 argument_type,分别是调用运算符的返回类型和参数类型
  • 默认构造函数和拷贝赋值运算符

​​  在定义此特例化版本的 hash 时,唯一复杂的地方是:必须在原模板定义所在的命名空间中特例化它。我们可以向命名空间添加成员,首先打开命名空间:

// 打开 std 命名空间
namespace std {  
}			// 关闭 std 命名空间,注意 } 之后没有分号

​花括号对之间的任何定义都将成为命名空间 std 的一部分
​​  ​然后定义一个能处理 Sales_data 的特例化 hash:

namespace std {
    template <>		// 指明我们正在特例化一个模板,参数类型是 Sales_data
    struct hash<Sales_data> {
        typedef size_t result_type;
        typedef Sales_data argument_type;		// 默认情况下,此类型需要 operator==
        size_t operator()(const Sales_data &s) const;
        // 这里使用合成的拷贝控制成员和默认构造函数
    };
    size_t hash<Sales_data>::operator()(const Sales_data &s) const {
        return hash<string>()(s.bookNo) ^
            hash<unsigned>()(s.units_sold) ^
            hash<double>()(s.revenue);
    }
}		// 关闭 std 命名空间,注意没有分号

类模板的显式(全)特例化定义以 template<> 开始,<>指出正在定义一个全特例化的模板。在本例中,我们正在特例化的模板名为 hash,而特例化版本为 hash<Sales_data>。接下来的类成员是按特例化 hash 的要求而定义的。

​​  类似其他任何类,我们可以在类内或类外定义特例化版本的成员,在本例中就是在类外定义的。

​​  默认情况下,为了处理特定关键字类型,无序容器会使用 key_type 对于的特例化 hash 版本和 key_type 上的 operator== 运算符

​​  ​由于 hash<Sales_data> 会使用 Sales_data 的私有成员,所以需要在 Sales_data 中将其声明为友元:

template <class T> class std::hash;	// 模板实例作为友元所需要的声明
class Sales_data {
friend class std::hash<Sales_data>;	// 模板实例作为友元
    // 其他成员
};

类模板部分特例化,模板的偏特化

​​  与函数模板不同类模板的特例化不必为所有模板参数都提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性(比如是否是 const 或左值和右值)。一个类模板的部分实例化(partial specialization,又称偏特化)本身是一个模板。在使用它时,用户必须为那些在部分特例化版本中未指定的模板参数提供实参

​​  ​标准库 remove_reference 就是通过一系列的特例化版本来完成其功能的:

// 原始的、最通用的版本
template <class T> struct remove_reference {
    typedef T type;
}
// 部分特例化版本,将用于左值引用和右值引用
template <class T> struct remove_reference<T&>
	{ typedef T type; }
template <class T> struct remove_reference<T&&>
	{ typedef T type; }

​​  ​由于一个部分特例化版本本质是一个模板,所以我们首先定义模板参数。同样,部分特例化版本的名字与原模板的名字相同。对每个未完成确定 类型/值 的模板参数,在特例化版本的模板形参列表中都必须有一项与它对应在类名之后,我们要为特例化的模板参数指定实参,这些实参列于模板名之后的尖括号对内。这些实参与原始模板中的模板参数按位置对应

template<class T1, class T2, int I>
class A1 {};            // 主模板
 
template<class T, int I>
class A1<T, T*, I> {};  // #1:部分特化,其中 T2 是指向 T1 的指针
 
template<class T, class T2, int I>
class A1<T*, T2, I> {}; // #2:部分特化,其中 T1 是指针
 
template<class T>
class A1<int, T*, 5> {}; // #3:部分特化,其中 T1 是 int,I 是 5,而 T2 是指针
 
template<class X, class T, int I>
class A1<X, T*, I> {};   // #4:部分特化,其中 T2 是指针
  • 在第一、二、四个特例化中,因为三个都是未确定类型或值的模板参数,所以在模板形参列表中都有一项与模板实参列表中的参数对应(T对应第一个第二个,I对应第三个)。
  • 在第三个特例化中,因为第一个和第三个模板形参都有确定的类型或值,所以不需要在模板形参列表中有对应的参数,可以省略模板形参列表中的那一项。

​​  ​即,部分特例化的模板参数列表是原始模板的参数列表的一个子集或者是一个特例化版本。还指将模板参数中的某个指定为具体的类型:

 //general template
template<class T1, class T2> 
class Pair { ... }; 

 //specialization with T2 set to int
template<class T1>
class Pair<T1, int>  { ... };

只特例化成员而不是整个类模板

​​  我们可以只特例化某一类模板的(无论是函数模板还是非模板)成员函数而不是特例化整个类模板。例如,如果 Foo 是一个模板类,包含一个成员 Bar,我们可以只特例化该成员:

template <typename T> struct Foo {
    Foo(const T& t = T()): mem(t) { }
    void Bar() { /* */ }
    T mem;
    // 其他成员
};
template <>
void Foo<int>::Bar() {	// 特例化 Foo<int> 的成员函数 Bar
    // 进行应用与 int 的处理
}

​​​  所以,当我们用 int 之外的任何类型使用 Foo 时,其成员像往常一样进行实例化。当我们用 int 使用 Foo 时,Bar 之外的成员向往常一样进行实例化。如果我们使用 Foo 的 Bar 成员,则会使用我们的特例化版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值