c++ primer 第十六章模板与泛型编程
16.1 定义模板
16.1.1 函数模板
函数模板是一个公式,可以来生成针对特定类型的函数版本。以template开始,后面跟一个模板参数列表,里面包含逗号分隔的模板参数。
模板参数是指类中用到的类型或值。使用模板时指定模板实参绑定到模板参数上。
调用函数模板时编译器根据函数实参来推断模板实参,然后用推断出的模板参数实例化一个特定版本的函数。称为模板的实例。
类型参数可以当做类型说明符使用,之前加class或者typename。两个关键字没有区别,typename较新。
模板中还有非类型参数,使用特定类型名定义。可以是整型或者是指向对象或函数类型的指针或(左值)引用。是指针或引用时实参必须有静态生存期。
函数模板中inline或者constexpr放在模板列表之后返回类型之前。
在使用函数模板时,函数参数是const的引用,可以用于不能拷贝的类型。函数体尽量使用与类型无关的代码,减少对于实参类型的要求。
编译器遇到模板定义时不生成代码,在遇到模板的使用时会生成特定类型的实例化代码。为了生成实例化版本的代码,编译器需要掌握函数模板或类模板成员函数的定义。因此模板的头文件通常包括声明和定义。
编译器会在三个阶段报告错误:
- 编译模板本身时。检查语法错误。
- 编译器遇到模板使用时。一般检查实参数目和参数类型是否匹配等。
- 模板实例化时,这个时候发现类型相关的错误。
16.1.2 类模板
类模板用来生成类的蓝图。与函数模板不同,编译器不能为类模板推断模板参数类型。
类模板使用时需要提供显示模板实参来实例化出特定的类。每个类型实例化出来的类互相独立。
类模板的成员函数和普通类的成员函数类似,但是每个实例化的类也对应着不同的实例化的成员函数,因此定义在类模板外的成员函数也需要template 开始加类型参数列表。
在模板内部直接使用类型形参不需要提供实参。
类模板有三种友元关系:
- 一对一友好关系:每个类型的类模板的实例对应使用相同类型的友元。
- 通用模板友好关系:每个模板的实例化都是另一个类的友元。这个类可能是非模板类或者是一个类模板任意的实例化。
- 特定的模板友好关系:一个类和使用该类的模板实例化是友元。
也可以在模板中将类型参数声明为友元。兼容对于内置类型的声明。
可以使用typedef来引用实例化的类,比如typedef Blob<string> StrBlob
;
新标准允许用类模板定义一个类型别名,如template<typename T> using twin = pair<T,T>
,定义一个类型别名时可以固定一个或多个模板参数。
类模板中的static成员对于每个实例化的类来说都有一个对应的实例化的静态成员。
16.1.3 模板参数
模板参数会隐藏外层作用域声明的相同名字,但是在模板内不能重用模板参数名。因此一个特定模板参数名在列表中只能出现一次。
模板声明必须含有模板参数。
当处理形如 T::size_type*p
的代码时我们需要知道语句表示的是变量定义还是一个乘法,因此需要标示清楚一个名字是否表示一个类型,可以在前面加上关键字typename来实现。
新标准允许为函数模板和类模板提供默认模板实参。
16.1.4 成员模板
一个类(无论是普通类还是模板类)可以包含本身是模板的成员函数。这种函数叫做成员模板。
特别的,对于类模板的成员模板,在类外定义时既要提供类模板参数列表也要提供成员函数模板参数列表。
16.1.5 控制实例化
显示实例化可以避免重复实例化同一种类。使用extern关键字表明当前模板在其他文件中有实例化,可以避免在当前文件中实例化。每个实例化的版本可以有多个extern声明,但只有一个定义。
实例化声明:extern template class Blob<string>;
实例化定义:template int compare(const int&, const int&);
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。预处理类模板的普通实例化不同,实例化定义会实例化该类的所有成员。
16.1.6 效率与灵活性
shared_ptr与unique_ptr的区别,前者共享指针,后者独占指针。
另一个差异是允许用户重载默认删除器的方式。shared_ptr只需要在创建时或reset时传递一个可调用的对象。而unique_ptr必须以显示模板实参的形式提供删除器的类型。
shared_ptr在运行时绑定删除器,需要保存为一个指针或封装指针的类。unique_ptr在编译时就绑定删除器,避免了间接的调用。两种方式一种更灵活,一种效率更高。
16.2 模板实参推断
16.2.1 类型转换与模板类型参数
传递给函数模板的实参只支持两种类型转换:
- const转换:可以将一个非const对象的引用或指针传递给一个const的引用或指针形参。
- 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参进行正常的指针转换。
其他类型转换如算数转换,都不能应用到函数模板调用。对于模板函数中非模板类型定义的参数也可以应用普通的类型转换。
16.2.2 函数模板显示实参
某些函数模板定义中无法推断出某些类型实参,需要显示指定,如template <typename T1, typename T2, typename T3> T1 sum(T2,T3)
中T1的类型无法推断出来,在调用时需要为T1提供显示实参。
显示模板实参按从左至右顺序与对应模板参数匹配,只有尾部参数可以不提供显示实参。
对于已经显示制定了类型的实参允许进行正常的类型转换。
16.2.3 尾置返回类型与类型转换
当需要用户确定函数模板的返回类型时,可以在函数模板参数列表尾部指定。比如使用decltype以及标准模板库中的类型转换类。
类型转换类,remove_reference,add_const等等。
16.2.4 函数指针与实参推断
当使用一个函数模板初始化一个函数指针或为其赋值,编译器使用指针的类型来推断模板实参。
如果可以确定需要的类型则对函数模板进行实例化,否则的话会编译失败。因此最好在调用时显示指出实例化的版本。
16.2.5 模板实参推断和引用
从左值引用推断实参类型:
template <typename T> void f(T&);
i是int,ci是const int。则:
f(i)中T是int。 f(ci)中T是const int。f(42)调用错误。
template <typename T> void f(const T&);
中:f(i) T是int。f(ci) T是int。f(42) T是int。
对于右值引用,一般只能传递右值或右值引用。但是C++允许在函数模板中将左值传递给右值引用,此时T推断为左值引用类型(第一个例外)。间接创建的引用的引用会导致引用折叠(例外二),折叠规律如下:
- X& &、X& && 和 X&& &都折叠为X&。
- 类型X&& &&折叠为X&&。
导致了两个重要结果:
- 如果一个函数参数是一个指向模板类型参数的右值引用,则它可以被绑定到一个左值;
- 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个普通左值引用参数。
16.2.6 理解std::move
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
首先形参为模板右值引用,因此可以接收任意类型的实参。然后使用remove_reference模板类来返回右值引用类型。
虽然不能隐式地将一个左值转换为右值引用,但是可以使用static_cast显示地将一个左值转换为一个右值引用。
16.2.7 转发
使用std::move可以保持原始实参再次调用时所有的类型都不变,包括const 左值或是右值引用相关信息。 一般直接使用std::forward。
16.3 重载与模板
函数模板可以被另一个函数模板或者非模板函数重载。涉及模板的函数匹配规则如下:
- 候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的函数模板总是可行的,因为模板实参推断会去掉不可行的模板。
- 可行函数按类型转换来排序。
- 如果只有一个精准匹配则选择它,否则若有多个同样好的匹配:若只有一个非模板函数则选择;若都是函数模板,选择最特例化的一个;否则调用有歧义。
参数T* 比 const T&参数更加特例化,对于同样精准的匹配优先T*。
对于函数模板与非模板函数同名重载时需要注意使用非模板函数时需要进行先声明,否则可能会调用实例化的函数模板。
16.4 可变参数模板
可变参数模板就是可以 接收可变数目参数的模板函数或模板类。可变数目的参数成为函数包:有模板参数包和函数参数包。
class… 或 typename…表示对于模板参数包的声明。
template<typename T, typename... Args>
void foo(const T &t, const Args&... rest);
Args是模板参数包,rest表示一个函数参数包,表示0个或多个函数参数。
sizeof…运算符返回一个表示参数包内参数数量的常量表达式。
16.4.1 编写可变参数函数模板
可变参数函数通常都是递归的,每次解开一个参数包中的参数。
16.4.2 包扩展
对于参数包除了获取大小操作之外只能够进行扩展。扩展一个包时还要提供每个扩展元素的模式。拓展一个包就是把它分解为其构成的元素,对每个元素应用模式,通过在模式后面加…实现。
直接拓展调用如:print(os,rest...)
加模式之后的调用如:print(os,debug_rep(rest)...)
…符号添加在模式调用的后面表示在模式应用之后进行扩展,否则表示为扩展之后再调用模式,可能会调用失败。
16.4.3 转发参数包
template <class... Args>
inline
void StrVec::emplace_back(Args&&... args) {
chk_n_alloc();
alloc.construct(first_free++,std::forward<Args>(args)...);
}
如果调用svec.emplace_back(10,‘c’)会拓展为std::forward<int>(10),std::forward<char>('c')
。
16.5 模板特例化
可以在模板的基础上对某种类型进行特殊的实例化来满足对特定类型的特殊处理,可以使用template<>前置符号表示对于某个特殊类型的模板的实例化,如:
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*。
一个特例化版本实质是是一个实例,而非函数名的一个重载版本。因此特例化的模板并不会影响函数匹配。
特例化模板需要在作用域中有原模板的声明。在使用特例化的模板之前也需要声明此特例化的模板。因此一般把同名模板声明都放在一起的一个头文件的前面,然后是它们的特例化版本。
特例化类模板以hash模板定义一个特例化版本为例:
hash模板特例化时需要打开原模板定义所在的命名空间中来特例化。
namespace std {
template<>
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.units_sold) ^
hash<double>()(s.revenue);
}
}
由于hash模板类需要调用Sales_data的私有成员,因此需要声明为该类的友元。
也可以将类模板中的模板参数进行部分特例化,提供一部分参数或者一部分特性。部分特例化的类模板依然是一个模板。比如标准库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;
}
也可以特例化模板的指定成员函数而不是整个模板。