模板是C++中泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。当使用一个vector这样的泛型类型,或者find这样的泛型函数时,我们提供足够的信息,将蓝图转换为特定的类或函数。这种转换发生在编译时。
一、定义模板
1. 函数模板
一个函数模板(function template)就是一个公式,可用来生成针对特定类型的函数版本。
模板定义以关键字template开始,后跟一个模板参数列表(template parameter list),这是一个逗号分隔的一个或多个模板参数的列表,用小于号(<)和大于号(>)包围起来。
在模板定义中,模板参数列表不能为空。
模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们(隐式地或显式地)指定模板实参(template argument),将其绑定到模板参数上。
实例化函数模板
当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。
编译器用推断出地模板参数来为我们实例化(instantiate)一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。
模板类型参数
类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换:
类型参数前必须使用关键字class或typename:
非类型模板参数
在模板中也可以定义非类型参数。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字class或typename来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。
在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数。例如,指定数组大小。
inline和constexpr的函数模板
模板编译
当我们使用(而不是定义)模板时,编译器才生成代码。
大多数编译错误在实例化期间报告
2. 类模板
类模板是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。为了使用类模板,我们必须在类模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
实例化类模板
当使用一个类模板时,我们必须提供额外信息。这些额外信息是显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板参数来实例化出特定的类。
类模板的成员函数
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。
在类代码内简化模板类名的使用
当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。
在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。
类模板和友元
当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元本身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友好关系
类模板与另一个(类或函数)模板间友好关系的最常见的形式是建立对应实例及其友元间的友好关系。
通用和特定的模板友好关系
令模板自己的类型参数成为友元
在新标准中,我们可以将模板类型参数声明为友元:
模板类型别名
类模板的一个实例定义了一个类类型,与任何其他类类型一样,我们可以定义一个typedef来引用实例化的类:
typedef Blob<string> StrBlob;
我们可以为类模板定义一个类型别名:
template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors是一个pair<string, string>
类模板的static成员
与任何其他static数据成员相同,模板类的每个static数据成员必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的static对象。
3. 模板参数
模板参数与作用域
模板参数遵循普通的作用域规则。但与大多数其他上下文不同,在模板内不能重用模板参数名。
模板声明
模板声明必须包含模板参数。
与函数参数相同,声明中的模板参数的名字不必与定义中相同。
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
使用类的类型成员
默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过使用关键字typename来实现这一点。
默认模板实参
我们可以为函数和类模板提供默认实参。
在这段代码中,我们为模板添加了第二个类型参数,名为F,表示可调用对象的类型;并定义了一个新的函数参数f,绑定到一个可调用对象上。
默认模板实参指出compare将使用标准库的less函数对象类,它是使用与compare一样的类型参数实例化的。默认函数实参指出f将是类型F的一个默认初始化的对象。
与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
模板默认实参与类模板
无论何时使用一个类模板,我们都必须在模板名之后接上尖括号。如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对。
4. 成员模板
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板(member template)。成员模板不能是虚函数。
普通(非模板)类的成员模板
类模板的成员模板
实例化与成员模板
为了实例化一个类模板的成员模板,我们必须同时提供类和函数模板的实参。与普通函数模板相同,编译器通常根据传递给成员模板的函数实参来推断它的模板实参。
5. 控制实例化
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化(explicit instantiation)来避免这种开销。一个显式实例化有如下形式:
实例化定义会实例化所有成员
6. 效率与灵活
在运行时绑定删除器
在编译时绑定删除器
二、模板实参推断
从函数实参来确定模板实参的过程被称为模板实参推断(template argument deduction)。
1. 类型转换与模板类型参数
顶层const无论是在形参中还是在实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括如下两项。
· const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
· 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。
其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。
fref调用不合法。如果形参是一个引用,则数组不会转换为指针。a和b的类型是不匹配的,因此调用是错误的。
使用相同模板参数类型的函数形参
一个模板类型参数可以用作多个函数形参的类型。由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。如果推断出的类型不匹配,则调用就是错误的。
正常类型转换应用于普通函数实参
函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。这种函数实参不进行特殊处理:它们正常转换为对应形参的类型。
2. 函数模板显式实参
在某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。
指定显式模板实参
正常类型转换应用于显式指定的实参
3. 尾置返回类型与类型转换
进行类型转换的标准库模板类
标准库的类型转换(type transformation)模板定义在头文件type_trait中。这个头文件中的类通常用于所谓的模板元程序设计。
如果不可能(或者不必要)转换模板参数,则type成员就是模板参数类型本身。例如,如果T是一个指针类型,则remove_pointer<T>::type是T指向的类型。如果T不是一个指针,则无须进行任何转换,从而type具有与T相同的类型。
4. 函数指针和实参推断
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
5. 模板实参推断和引用
从左值引用函数参数推断类型
当一个函数参数是模板类型参数的一个普通(左值)引用时(即,形如T&),绑定规则告诉我们只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。
从右值引用函数参数推断类型
引用折叠和右值引用参数
假定i是一个int对象。
C++在正常绑定规则之外定义了两个例外规则。
第一个例外规则:当我们将一个左值(如i)传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。因此当我们调用f3(i)时,编译器推断T的类型为int&,而非int。
第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即,对于一个给定类型X:
· X& &、 X& && 和X&& &都折叠成类型X&
· 类型X&& &&折叠成X&&
编写接受右值引用参数的模板函数
在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。
使用右值引用的函数模板通常使用以下方式来进行重载:
6. 理解std::move
虽然不能直接将一个右值引用绑定到一个左值上,但可以用move获得一个绑定到左值上的右值引用。
std::move是如何定义的
标准库是这样定义move的:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
move的函数参数T&&是一个指向模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。特别地,我们既可以传递给move一个左值,也可以传递给它一个右值:
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // 正确:从一个右值移动数据
s2 = std::move(s1); // 正确:但在赋值之后,s1的值是不确定的
从一个左值static_cast到一个右值引用是允许的
虽然不能隐式地将一个左值转换为右值引用,但我们可以用static_cast显式地将一个左值转换为一个右值引用。
7. 转发
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。
定义能保持类型信息的函数参数
在调用中使用std::forward保持类型信息
三、重载与模板
重载模板和类型转换
f(p)的调用与预期不符,预期调用f(const T*), 实际调用f(T)。分析原因:调用f(const T*)还需要进行const转换,而调用f(T)实例化为f(int*)。
四、可变参数模板
一个可变参数模板(variadic template)就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包(template packet)。存在两种参数包:模板参数包(template parameter packet),表示零个或多个模板参数;函数参数包(function parameter packet),表示零个或多个函数参数。
我们用一个省略号来指出一个模板参数或函数参数表示一个包。再一个模板参数列表中,class...或typename...指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
与往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目。
sizeof...运算符
当我们需要知道包中有多少元素时,可以使用sizeof...运算符。类似sizeof,sizeof...也返回一个常量表达式,而且不会对其实参求值。
1. 编写可变参数函数模板
我们可以使用initializer_list来定义一个可接受可变数目实参的函数。但是,所有实参必须具有相同的类型(或者它们的类型可以转换为同一个公共类型)。当我们既不知道想要处理的实参的数目也不知道它们的类型时,可变参数函数是很有用的。
可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
2. 包扩展
扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放置一个省略号(...)来触发扩展操作。
3. 转发参数包
五、模板特例化
当我们不能(或不希望)使用模板版本时,可以定义类或函数模板的一个特例化版本。
定义函数模板特例化
当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应使用关键字template后跟一个空尖括号对(<>)。空尖括号指出我们将为原模板的所有模板参数提供实参:
当我们定义一个特例化版本时,函数参数类型必须与一个先前声明的模板中对应的类型匹配。
一个特例化版本本质上是一个实例,而非函数名的一个重载版本。特例化不影响函数匹配。
类模板特例化
举例:为标准库hash模板定义一个特例化版本,可以用它来将Sales_data对象保存在无序容器中。默认情况下,无序容器使用hash<key_type>来组织元素。为了让我们自己的数据类型也能使用这种默认组织方式,必须定义hash模板的一个特例化版本。一个特例化hash类必须定义:
在定义此特例化版本的hash时,唯一复杂的地方是:必须在原模板定义所在的命名空间中特例化它。我们可以向命名空间添加成员。
// 打开std命名空间,以便特例化std::hash
// 花括号对之间的任何定义都将成为命名空间std的一部分
namespace std {
template <> // 我们正在定义一个特例化版本,模板参数为Sales_data
struct hash<Sales_data>
{
// 用来散列一个无序容器的类型必须要定义以下类型
typedef size_t result_type;
typedef Sales_data argument_type; // 默认情况下,此类型需要==
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.unsigned) ^ hash<double>()(s.revenue);
}
} // 关闭std命名空间
类模板部分特例化
与函数模板不同,类模板的特例化不必为所有模板参数提供实参,我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化(partial specialization)本身是一个模板,使用它时用户还必须为那些在特例化版本中为指定的模板参数提供实参。