文章目录
模板参数
类似函数参数的名字,一个模板参数的名字也没有什么内在含义。我们通常将类型参数命名 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; // 错误,重声明
}
由于模板参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次:
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) { /* */ }
使用类的类型成员 (typedef typename T::size_type szt为什么要 typename)
在非模板代码中,我们用作用域运算符 ( :: ) 来访问 static 成员和类型成员,编译器知道作用域访问的名字是类型还是 static 成员。例如,string::size_type,编译器有 string 的定义,编译器知道 size_type 是一个类型而非成员。
但是模板中,就不同了。例如,假定 T 是一个模板类型参数,当编译器遇到类似 T::mem 的代码时,它不会知道 mem 是一个类型成员还是 static 数据成员,直到实例化时才会知道。但是,为了处理模板,编译器必须知道名字是否表示一个类型。例如,假定 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) {
/* ... */
}
当我们希望通知编译器一个名字表示类型时,必须使用关键字 typename,不能使用 class。
默认模板实参 (类似 sort 的第三个实参)
我们可以提供默认模板实参。在新标准中,可以为函数和类模板提供默认实参。在更早的版本只允许为类模板提供默认实参。
例如,默认使用标准库的 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;
}
这里我们的 compare 提供了一个名为 F 的模板实参,表示一个可调用对象,其默认参数是 less 。这样,当我们调用 compare 时,便可以提供自己的比较操作,但这不是必须的。假设有:
struct A { int a; char c; };
我们知道,A 没有定义 operator <,所以我们不能直接使用 compare,但我们可以提供一个可调用对象,如:
bool cmp(const A &a,const A &b) {
return a.c < b.c;
}
我们便可以使用 compare 函数:
A a,b;
cout << compare(a,b,cmp);
其实这很像为 sort 提供的第三个参数 (提供一个可调用对象来进行排序,默认的比较运算符是 <)。
与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以是默认实参。
模板默认实参与类模板
无论何时使用一个类模板,我们都必须在模板名之后接上尖括号。尖括号指出类必须从一个模板实例化而来。特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个尖括号对:
template <typename T = int> class Numbers { // T 默认为 int
public:
Numbers(T v = 0): val(v) { }
private:
T val;
}
Numbers<long double> lots_of_precision;
Numbers<> average_precision; // 空的 <> 表示我们希望使用默认类型、
上面代码中,使用默认类型时,我们的尖括号对还是必须要有的。
成员模版
一个类 (普通类或类模板) 可以包含本身时模板的成员函数。这种成员被称为成员模板。成员模板不能是虚函数。
普通类的成员模板
作为普通类包含成员模板的例子,我们定义一个类,类似 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 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());
控制实例化 (实例化声明与定义)
当模板被使用时才会进行实例化,这一特性意味着,相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化来避免这种开销。一个显式实例化有以下形式:
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> 以及接受 initializer_list 的构造函数在本文件中实例化
Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};
Blob<int> a2(a1); // 拷贝构造函数在本文件中实例化
int i = compare(a1[0],a2[0]); // 实例化出现在其他位置
我们在 templateBuild.cc zhong 实例化上述模板,并定义:
// templateBuild.cc
// 实例化文件必须为每个在其他文件中声明为 extern 的类型和函数提供一个 (非 extern) 的定义
template int compare(const int&,const int&);
template class Blob<string>; // 实例化类模板的所有成员
对每一个实例化声明,在程序中某个位置必须由其显式的实例化定义。
实例化定义会实例化所有成员
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。当编译器遇到一个实例化定义时,它不了解程序使用哪些成员函数。因此,与处理类模板的普通实例化不同,编译器会实例化该类的所有成员。
效率与灵活性
对模板设计者所面对的设计选择,标准库智能指针类型给出了一个很好的展示。
shared_ptr 和 unque_ptr 之间的明显不同是它们管理所保存的指针的策略——前者给予我们共享指针所有权的能力;后者则独占指针。
这两个类的另一个差异是它们允许用户重载默认删除器方式。我们可以很容易地重载一个 shared_ptr 的删除器,只要在创建或 reset 指针时传递给它一个可调用对象即可。与之相反,删除器的类型是一个 unique_ptr 对象类型的一部分。用户必须在定义 unique_ptr 时以显示模板实参的形式提供删除器类型。因此,对于 unique_ptr 的用户来说,提供自己的删除器就更为复杂。
如果处理删除器的差异实际上就是这两个类功能的差异。但是,如我们将要看到的,这一实现策略上的差异可能对性能有重要的影响。
在运行时绑定删除器 (shared_ptr 删除器的工作方式)
虽然我们不知道标准库的具体实现,但可以推断出,shared_ptr 必须能直接访问其删除器。即,删除器必须保存为一个指针或一个封装了指针的类。
我们可以确定 shared_ptr 不是将删除器直接保存为一个成员,因为删除器的类型知道运行时才会知道。实际上,在一个 shared_ptr 的生存期中,我们可以随时改变其删除器的类型。我们可以使用一种类的删除器构造一个 shared_ptr,随后使用 reset 赋予此 shared_ptr 另一种类型的删除器。通常,类成员的类型在运行时是不能改变的。因此,不能直接保存删除器。
为了考察删除器是如何正确工作的,我们假定 shared_ptr 将它所管理的指针保存在一个成员 p 中,且删除器是通过一个名为 del 的成员来访问的。则 shared_ptr 的析构函数必须包含类似以下语句:
// del 的值只有在运行时才知道;通过一个指针来调用它
del ? del(p) : delete p;
在编译时绑定删除器 (unique_ptr 删除器的工作方式)
现在,让我们来考察 unique_ptr 可能的工作方式。在这个类中,删除器类型是类类型的一部分。即,unique_ptr 有两个模板参数,一个表示它所管理的指针,另一个表示删除器类型。由于删除器的类型是 unique_ptr 的一部分,因此删除器成员的类型在编译时是知道的,从而删除器可以保存在 unique_ptr 对象中。
unique_ptr 的析构函数与 shared_ptr 的析构函数类似,也是对其保存的指针调用用户提供的删除器或执行 delete:
// del 在编译时绑定;直接调用实例化的删除器
del(p); // 无运行时额外开销
通过在编译时绑定删除器,unique_ptr 避免了间接调用删除器的运行时开销。通过在运行时绑定删除器,shared_ptr 使用户重载删除器更为方便。