对模板设计者所面对的设计选择,标准库智能指针给出了一个很好的展示。
shared_ptr和unique_ptr之间的明显不同是它们管理所保存的指针策略------前者给予我们共享指针所有权的能力;后者则独占指针。这一差异对两个类的功能来说是至关重要的。
这两个类的另一个差异是他们允许用户重载默认删除器的方式。我们很容易地重载一个shared_ptr的删除器,只要在创建或者reset指针时传递给它一个可调用对象即可。与之相反,删除器的类型是unique_ptr对象类型的一部分。用户必须在定义时以显示模板实参的形式提供删除器的类型。因此,对于unique_ptr的用户来说,提供自己的删除器就更为复杂。
如何处理删除器的差异实际上就是这两个类功能上的差异。但是,如我们将要看到的,这一实现策略上的差异可能对性能产生重要影响。
在运行时绑定删除器
虽然我们不知道标准库类型是如何实现的,但可以推断出,shared_ptr必须能直接访问删除器。即删除器必须保存为一个指针或者一个封装了指针的类(如function)。
我们可以确定不是将删除器直接保存为一个成员,因为删除器的类型直到运行时才会知道。实际上,在一个shared_ptr的生存期中,我们可以随时改变删除器的类型。我们可以使用一种类型的删除器构造一个shared_ptr,随后使用reset赋予此shared_ptr另一种类型的删除器。通常,类成员的类型在运行时是不能改变的。因此,不能直接保存删除器。
为了考察删除器是如何工作的,让我们假定shared_ptr将它管理的指针保存在一个成员p中,且删除器是通过一个名为del的成员来访问的。则shared_ptr的析构函数必须包含类似下面这样的语句:
//del的值只有运行时才知道;通过一个指针来调用它 del ? del(p) : delete p; //del(p)需要运行时跳转到del的地址
由于删除器是间接保存的,调用del(p)需要一次运行时的跳转操作,转到del中保存的地址来执行对应的代码。
在编译时绑定删除器
现在,让我们来考察unique_ptr可能的工作方式。在这个类中,删除器的类型是类类型的一部分。即,unique有两个模板参数,一个表示它所管理的指针,另一个表示删除器的类型。由于删除器的类型是unique_ptr类型的一部分,因此删除器的成员类型在编译时是知道的,从而删除器可以直接保存在unique_ptr对象中。
unique_ptr的析构函数与shared_ptr的析构函数类似,也是对其保存的指针调用用户提供的删除器或执行delete。
//del在编译时绑定;直接调用实例化删除器 del(p); //无运行时额外开销
del的类型是默认删除器类型,或者用户提供的类型。到底是哪种情况没有关系,应该执行的代码在编译时肯定知道。实际上,如果删除器是类似DebugDelete之类的东西,这个调用可能被编译为内联形式。
通过在编译时绑定删除器,unique_ptr避免了间接调用删除器的运行时开销。通过在运行时绑定删除器,shared_ptr使用户重载删除器更加方便。