C++模板与泛型编程:模板参数、成员模板、控制实例化(实例化声明与定义)、效率与灵活性(智能指针删除器的工作方式)

模板参数

​ 类似函数参数的名字,一个模板参数的名字也没有什么内在含义。我们通常将类型参数命名 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 使用户重载删除器更为方便。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值