灵巧(smart)指针
灵巧指针是一种外观和行为都被设计成与内建指针相类似的对象,不过它能提供更多的功能。它们有许多应用的领域,包括资源管理(参见条款9、10、25和31)和重复代码任务的自动化(参见条款17和29)
当你使用灵巧指针替代C++的内建指针(也就是dumb pointer),你就能控制下面这些方面的指针的行为:
构造和析构。你可以决定建立灵巧指针时应该怎么做。通常赋给灵巧指针缺省值0,避免出现令人头疼的未初始化的指针。当指向某一对象的最后一个灵巧指针被释放时,一些灵巧指针负责删除它们指向的对象。这样做对防止资源泄漏很有帮助。
拷贝和赋值。你能对拷贝灵巧指针或设计灵巧指针的赋值操作进行控制。对于一些类型的灵巧指针来说,期望的行为是自动拷贝它们所指向的对象或用对这些对象进行赋值操作,也就是进行deep copy(深层拷贝)。对于其它的一些灵巧指针来说,仅仅拷贝指针本身或对指针进行赋值操作。还有一部分类型的灵巧指针根本就不允许这些操作。无论你认为应该如何去做,灵巧指针始终受你的控制。
Dereferencing(取出指针所指东西的内容)。当客户端引用被灵巧指针所指的对象,会发生什么事情呢?你可以自行决定。例如你可以用灵巧指针实现条款17提到的lazy fetching 方法。
灵巧指针从模板中生成,因为要与内建指针类似,必须是strongly typed(强类型)的;模板参数确定指向对象的类型。大多数灵巧指针模板看起来都象这样:
class SmartPtr {
public:
SmartPtr(T* realPtr = 0); // 建立一个灵巧指针
// 指向dumb pointer所指的
// 对象。未初始化的指针
// 缺省值为0(null)
SmartPtr(const SmartPtr& rhs); // 拷贝一个灵巧指针
~SmartPtr(); // 释放灵巧指针
// make an assignment to a smart ptr
SmartPtr& operator=(const SmartPtr& rhs);
T* operator->() const; // dereference一个灵巧指针
// 以访问所指对象的成员
T& operator*() const; // dereference 灵巧指针
private:
T *pointee; // 灵巧指针所指的对象
} ;
拷贝构造函数和赋值操作符都被展现在这里。对于灵巧指针类来说,不能允许进行拷贝和赋值操作,它们应该被声明为private(参见Effective C++条款27)。两个dereference操作符被声明为const,是因为dereference一个指针时不能对指针进行修改(尽管可以修改指针所指的对象)。最后,每个指向T对象的灵巧指针包含一个指向T的dumb pointer。这个dumb pointer指向的对象才是灵巧指针指向的真正对象。
进入灵巧指针实作的细节之前,应该研究一下客户端如何使用灵巧指针。考虑一下,存在一个分布式系统(即其上的对象一些在本地,一些在远程)。相对于访问远程对象,访问本地对象通常总是又简单而且速度又快,因为远程访问需要远程过程调用(RPC),或其它一些联系远距离计算机的方法。
对于编写程序代码的客户端来说,采用不同的方法分别处理本地对象与远程对象是一件很烦人的事情。让所有的对象都位于一个地方会更方便。灵巧指针可以让程序库实现这样的梦想。
class DBPtr { // 中对象的灵巧指针模板
public:
DBPtr(T *realPtr = 0); // 建立灵巧指针,指向
// 由一个本地dumb pointer
// 给出的DB 对象
DBPtr(DataBaseID id); // 建立灵巧指针,
// 指向一个DB对象,
// 具有惟一的DB识别符
... // 其它灵巧指针函数
} ; // 同上
class Tuple { // 数据库元组类
public:
...
void displayEditDialog(); // 显示一个图形对话框,
// 允许用户编辑元组。
// user to edit the tuple
bool isValid() const; // 返回*this是否通过了
} ; // 合法性验证
// 这个类模板用于在修改T对象时进行日志登记。
// 有关细节参见下面的叙述:
template < class T >
class LogEntry {
public:
LogEntry(const T& objectToBeModified);
~LogEntry();
} ;
void editTuple(DBPtr < Tuple >& pt)
{
LogEntry<Tuple> entry(*pt); // 为这个编辑操作登记日志
// 有关细节参见下面的叙述
// 重复显示编辑对话框,直到提供了合法的数值。
do {
pt->displayEditDialog();
} while (pt->isValid() == false);
}
在editTuple中被编辑的元组物理上可以位于本地也可以位于远程,但是编写editTuple的程序员不用关心这些事情。灵巧指针类隐藏了系统的这些方面。程序员只需关心通过对象进行访问的元组,而不用关心如何声明它们,其行为就像一个内建指针。
注意在editTuple中LogEntry对象的用法。一种更传统的设计是在调用displayEditDialog前开始日志记录,调用后结束日志记录。在这里使用的方法是让LogEntry的构造函数启动日志记录,析构函数结束日志记录。正如条款9所解释的,当面对异常时,让对象自己开始和结束日志记录比显示地调用函数可以使的程序更健壮。而且建立一个LogEntry对象比每次都调用开始记录和结束记录函数更容易。
正如你所看到的,使用灵巧指针与使用dump pointer没有很大的差别。这表明了封装是非常有效的。灵巧指针的客户端可以象使用dumb pointer一样使用灵巧指针。正如我们将看到的,有时这种替代会更透明化。
灵巧指针的构造、赋值和析构
灵巧指针的的析构通常很简单:找到指向的对象(一般由灵巧指针构造函数的参数给出),让灵巧指针的内部成员dumb pointer指向它。如果没有找到对象,把内部指针设为0或发出一个错误信号(可以是抛出一个异常)。
灵巧指针拷贝构造函数、赋值操作符函数和析构函数的实作由于所有权的问题所以有些复杂。如果一个灵巧指针拥有它指向的对象,当它被释放时必须负责删除这个对象。这里假设灵巧指针指向的的对象是动态分配的。这种假设在灵巧指针中是常见的(有关确定这种假设是真实的方法,参见条款27)。
看一下标准C ++ 类库中auto_ptr模板。这如条款9所解释的,一个auto_ptr对象是一个指向堆对象的灵巧指针,直到auto_ptr被释放。auto_ptr的析构函数删除其指向的对象时,会发生什么事情呢?auto_ptr模板的实作如下:
template < class T >
class auto_ptr {
public:
auto_ptr(T *ptr = 0): pointee(ptr) {}
~auto_ptr() { delete pointee; }
...
private:
T *pointee;
} ;
假如auto_ptr拥有对象时,它可以正常运行。但是当auto_ptr被拷贝或被赋值时,会发生什么情况呢?
auto_ptr < TreeNode > ptn1( new TreeNode);
auto_ptr < TreeNode > ptn2 = ptn1; // 调用拷贝构造函数
// 会发生什么情况?
auto_ptr < TreeNode > ptn3;
ptn3 = ptn2; // 调用 operator=;
// 会发生什么情况?
如果我们只拷贝内部的dumb pointer,会导致两个auto_ptr指向一个相同的对象。这是一个灾难,因为当释放quto_ptr时每个auto_ptr都会删除它们所指的对象。这意味着一个对象会被我们删除两次。这种两次删除的结果将是不可预测的(通常是灾难性的)。
另一种方法是通过调用new,建立一个所指对象的新拷贝。这确保了不会有许多指向同一个对象的auto_ptr,但是建立(以后还得释放)新对象会造成不可接受的性能损耗。并且我们不知道要建立什么类型的对象,因为auto_ptr<T>对象不用必须指向类型为T的对象,它也可以指向T的派生类型对象。虚拟构造函数(参见条款25)可能帮助我们解决这个问题,但是好象不能把它们用在auto_ptr这样的通用类中。
如果quto_ptr禁止拷贝和赋值,就可以消除这个问题,但是采用“当auto_ptr被拷贝和赋值时,对象所有权随之被传递”的方法,是一个更具灵活性的解决方案:
class auto_ptr {
public:
...
auto_ptr(auto_ptr<T>& rhs); // 拷贝构造函数
auto_ptr<T>& // 赋值
operator=(auto_ptr<T>& rhs); // 操作符
...
} ;
template < class T >
auto_ptr < T > ::auto_ptr(auto_ptr < T >& rhs)
{
pointee = rhs.pointee; // 把*pointee的所有权
// 传递到 *this
rhs.pointee = 0; // rhs不再拥有
} // 任何东西
template < class T >
auto_ptr < T >& auto_ptr < T > :: operator = (auto_ptr < T >& rhs)
{
if (this == &rhs) // 如果这个对象自我赋值
return *this; // 什么也不要做
delete pointee; // 删除现在拥有的对象
pointee = rhs.pointee; // 把*pointee的所有权
rhs.pointee = 0; // 从 rhs 传递到 *this
return *this;
}
注意赋值操作符在接受新对象的所有权以前必须删除原来拥有的对象。如果不这样做,原来拥有的对象将永远不会被删除。记住,除了auto_ptr对象,没有人拥有auto_ptr指向的对象。
因为当调用auto_ptr的拷贝构造函数时,对象的所有权被传递出去,所以通过传值方式传递auto_ptr对象是一个很糟糕的方法。因为:
void printTreeNode(ostream & s, auto_ptr < TreeNode > p)
{ s << *p; }
int main()
{
auto_ptr<TreeNode> ptn(new TreeNode);
...
printTreeNode(cout, ptn); //通过传值方式传递auto_ptr
...
}
当printTreeNode的参数p被初始化时(调用auto_ptr的拷贝构造函数),ptn指向对象的所有权被传递到给了p。当printTreeNode结束执行后,p离开了作用域,它的析构函数删除它指向的对象(就是原来ptr指向的对象)。然而ptr不再指向任何对象(它的dumb pointer是null),所以调用printTreeNode以后任何试图使用它的操作都将产生不可定义的行为。只有在你确实想把对象的所有权传递给一个临时的函数参数时,才能通过传值方式传递auto_ptr。这种情况很少见。
这不是说你不能把auto_ptr做为参数传递,这只意味着不能使用传值的方法。通过const引用传递(Pass-by-reference-to-const)的方法是这样的:
void printTreeNode(ostream & s,
const auto_ptr < TreeNode >& p)
{ s << *p; }
在函数里,p是一个引用,而不是一个对象,所以不会调用拷贝构造函数初始化p。当ptn被传递到上面这个printTreeNode时,它还保留着所指对象的所有权,调用printTreeNode以后还可以安全地使用ptn。从而通过const引用传递auto_ptr可以避免传值所产生的风险。(“引用传递”替代“传值”的其他原因参见Effective C++条款22)。
在拷贝和赋值中,把对象的所有权从一个灵巧指针传递到另一个中去,这种思想很有趣,而且你可能已经注意到拷贝构造函数和赋值操作符不同寻常的声明方法同样也很有趣。这些函数同上会带有const参数,但是上面这些函数则没有。实际上在拷贝和赋值中上述这些代码修改了这些参数。也就是说,如果auto_ptr对象被拷贝或做为赋值操作的数据源,就会修改auto_ptr对象!
是的,就是这样。C++是如此灵活能让你这样去做,真是太好了。如果语言要求拷贝构造函数和赋值操作符必须带有const参数,你必须去掉参数的const属性(参见Effective C++条款21)或用其他方法实现所有权的转移。准确地说:当拷贝一个对象或这个对象做为赋值的数据源,就会修改该对象。这可能有些不直观,但是它是简单的,直接的,在这种情况下也是准确的。
如果你发现研究这些auto_ptr成员函数很有趣,你可能希望看看完整的实作。在291页至294页上有(只原书页码),在那里你也能看到在标准C++库中auto_ptr模板有比这里所描述的更灵活的拷贝构造函数和赋值操作符。在标准C++库中,这些函数是成员函数模板,不只是成员函数。(在本条款的后面会讲述成员函数模板。也可以阅读Effective C++条款25)。
灵巧指针的析构函数通常是这样的:
SmartPtr < T > :: ~ SmartPtr()
{
if (*this owns *pointee) {
delete pointee;
}
}
有时删除前不需要进行测试,例如在一个auto_ptr总是拥有它指向的对象时。而在另一些时候,测试会更为复杂。一个使用了引用计数(参见条款29)灵巧指针必须在判断是否有权删除所指对象前调整引用计数值。当然还有一些灵巧指针象dumb pointer一样,当它们被删除时,对所指对象没有任何影响。
实作Dereference 操作符
让我们把注意力转向灵巧指针的核心部分,the operator* 和 operator-> 函数。前者返回所指的对象。理论上,这很简单:
T & SmartPtr < T > :: operator * () const
{
perform "smart pointer" processing;
return *pointee;
}
首先无论函数做什么,必须先初始化指针或使pointee合法。例如,如果使用lazy fetch(参见条款17),函数必须为pointee建立一个新对象。一旦pointee合法了,operator*函数就返回其所指对象的一个引用。
注意返回类型是一个引用。如果返回对象,尽管编译器允许这么做,这也将会导致灾难性后果。必须时刻牢记:pointee不用必须指向T类型对象;它也可以指向T的派生类对象。如果在这种情况下operator*函数返回的是T类型对象而不是派生类对象的引用,你的函数实际上返回的是一个错误类型的对象!(这是一个slicing问题,参见Effective C++条款22和本书条款13)。在返回的这种对象上调用虚拟函数,不会触发与所指对象的动态类型相符的函数。实际上就是说你的灵巧指针不能支持虚拟函数,象这样的指针再灵巧也没有用。而返回一个引用还能够具有更高的效率(不需要构造一个临时对象,参见条款19)。能够兼顾正确与效率当然是一件好事。
如果你是一个急性子的人,你可能会想如果一些人在null灵巧指针上调用operator*,也就是说灵巧指针的dumb pointer是null。放松。随便做什么都行。dereference一个空指针的结果是未定义的,所以这不是一个“错误”的行为。想排除一个异常么?可以,抛出吧。想调用abort函数(可能被assert在失败时调用)?好的,调用吧。想遍历内存把每个字节都设成你生日与256模数么?当然也可以。虽说这样做没有什么好处,但是就语言本身而言,你完全是自由的。
operator->的情况与operator*是相同的,但是在分析operator->之前,让我们先回忆一下这个函数调用的与众不同的含义。再考虑editTuple函数,其使用一个指向Tuple对象的灵巧指针:
{
LogEntry<Tuple> entry(*pt);
do {
pt->displayEditDialog();
} while (pt->isValid() == false);
}
语句
pt -> displayEditDialog();
被编译器解释为:
(pt. operator -> ()) -> displayEditDialog();
这意味着不论operator->返回什么,它必须使用member-selection operator(成员选择操作符)(->)。因此operator->仅能返回两种东西:一个指向某对象的dumb pointer或另一个灵巧指针。多数情况下,你想返回一个普通dumb pointer。在此情况下,你这样实作operator-> :
T * SmartPtr < T > :: operator -> () const
{
perform "smart pointer" processing;
return pointee;
}
这样做运行良好。因为该函数返回一个指针,通过operator->调用虚拟函数,其行为也是正确的。
对于很多程序来说,这就是你需要了解灵巧指针的全部东西。条款29的引用计数代码并没有比这里更多的功能。但是如果你想更深入地了解灵巧指针,你必须知道更多的有关dumb pointer的知识和灵巧指针如何能或不能进行模拟。如果你的座右铭是“Most people stop at the Z-but not me(多数人浅尝而止,但我不能够这样)”,下面讲述的内容正适合你。
测试灵巧指针是否为NULL
目前为止我们讨论的函数能让我们建立、释放、拷贝、赋值、dereference灵巧指针。但是有一件我们做不到的事情是“发现灵巧指针为NULL”:
...
if (ptn == 0 ) ... // error!
if (ptn) ... // error!
if ( ! ptn) ... // error!
这是一个严重的限制。
在灵巧指针类里加入一个isNull成员函数是一件很容易的事,但是仍然没有解决当测试NULL时灵巧指针的行为与dumb pointer不相似的问题。另一种方法是提供隐式类型转换操作符,允许编译上述的测试。一般应用于这种目的的类型转换是void* :
class SmartPtr {
public:
...
operator void*(); // 如果灵巧指针为null,
... // 返回0, 否则返回
} ; // 非0。
SmartPtr < TreeNode > ptn;
...
if (ptn == 0 ) ... // 现在正确
if (ptn) ... // 也正确
if ( ! ptn) ... // 正确
这与iostream类中提供的类型转换相同,所以可以这样编写代码:
if (inputFile) ... // 测试inputFile是否已经被
// 成功地打开。
像所有的类型转换函数一样,它有一个缺点,在一些情况下虽然大多数程序员希望它调用失败,但是函数还能够成功地被调用(参见条款5)。特别是它允许灵巧指针与完全不同的类型之间进行比较:
SmartPtr < Orange > po;
...
if (pa == po) ... // 这能够被成功编译!
即使在SmartPtr<Apple> 和 SmartPtr<Orange>之间没有operator= 函数,也能够编译,因为灵巧指针被隐式地转换为void*指针,对于内建指针类型有一个内建的比较函数。这种进行隐式类型转换的行为特性很危险。(再看一下条款5,必须反反复复地阅读,做到耳熟能详。)
在void*类型转换方面,也有一些变通之策。有些设计者采用到const void*的类型转换,还有一些采取转换到bool的方法。这些变通之策都没有消除混合类型比较的问题。
有一种两全之策可以提供合理的测试空值的语法形式,同时把不同类型的灵巧指针之间进行比较的可能性降到最低。这就是在灵巧指针类中重载operator!,当且仅当灵巧指针是一个空指针时operator!返回true:
class SmartPtr {
public:
...
bool operator!() const; // 当且仅当灵巧指针是
... // 空值,返回true。
} ;
客户端程序如下所示:
SmartPtr < TreeNode > ptn;
...
if ( ! ptn) { // 正确
... // ptn 是空值
}
else {
... // ptn不是空值
}
但是这样就不正确了:
if (ptn) ... // 也是错误的
仅在这种情况下会存在不同类型之间进行比较:
SmartPtr < Orange > po;
...
if ( ! pa == ! po) ... // 能够编译
幸好程序员不会经常这样编写代码。有趣的是,iostream库的实作除了提供void*隐式的类型转换,也有operator!函数,不过这两个函数被用于测试的流状态有些不同。(在C++类库标准中(参见Effective C++ 条款49和本书条款35),void*隐式的类型转换已经被bool类型的转换所替代,operator bool总是返回与operator!相反的值。)
把灵巧指针转变成dumb指针
有时你要在一个程序里或已经使用dumb指针的程序库中添加灵巧指针。例如,你的分布式数据库系统原来不是分布式的,所以可能有一些老式的库函数没有使用灵巧指针:
SmartPtr < Orange > po;
...
if ( ! pa == ! po) ... // 能够编译
考虑一下,如果你试图用指向Tuple的灵巧指针调用normalize,会出现什么情况
...
normalize(pt); // 错误!
这种调用不能够编译,因为不能把DBPtr<Tuple>转换成Tuple*。你可以这样做,从而使该该函数正常运行:
不过我觉得你会讨厌这种调用方式。
在灵巧指针模板中增加指向T的dumb指针的隐式类型转换操作符,可以让以上函数调用成功运行:
class DBPtr {
public:
...
operator T*() { return pointee; }
...
} ;
DBPtr < Tuple > pt;
...
normalize(pt); // 能够运行
并且这个函数也消除了测试空值的问题:
// Tuple*
if (pt) ... // 同上
if ( ! pt) ... // 同上 (reprise)
然而,它也有类型转换函数所具有的缺点(几乎总是这样,看条款5)。它使得客户端能够很容易地直接访问dumb指针,绕过“类指针(pointer-like)”对象所提供的“灵巧”特性:
{
Tuple *rawTuplePtr = pt; // 把DBPtr<Tuple> 转变成
// Tuple*
使用raw TuplePtr 修改 tuple;
}
通常,灵巧指针提供的“灵巧”行为特性是设计中的主要组成部分,所以允许客户端使用dumb指针会导致灾难性的后果。例如,如果DBPtr实现了条款29中引用计数的功能,允许客户端直接对dumb指针进行操作很可能破坏“引用计数”数据结构,而导致引用计数错误。
甚至即使你提供一个从灵巧指针到dumb指针的隐式转换操作符,灵巧指针也不能真正地做到与dumb指针互换。因为从灵巧指针到dumb指针的转换是“用户定义类型转换”,在同一时间编译器进行这种转换的次数不能超过一次。例如假设有一个表示能够访问某一元组的所有客户的类:
public:
TupleAccessors(const Tuple *pt); // pt identifies the
... // tuple whose accessors
} ; // we care about
通常,TupleAccessors的单参数构造函数也可以做为从Tuple*到TupleAccessors的类型转换操作符(参见条款5)。现在考虑一下用于合并两个TupleAccessors对象内信息的函数:
const TupleAccessors & ta2);
因为一个Tuple*可以被隐式地转换为TupleAccessors,用两个dumb Tuple*调用merge函数,可以正常运行:
...
merge(pt1, pt2); // 正确, 两个指针被转换为
// TupleAccessors objects
如果用灵巧指针DBPtr<Tuple>进行调用,编译就会失败:
...
merge(pt1, pt2); // 错误!不能把 pt1 和
// pt2转换称TupleAccessors对象
因为从DBPtr<Tuple>到TupleAccessors的转换要调用两次用户定义类型转换(一次从DBPtr<Tuple>到Tuple*,一次从Tuple*到TupleAccessors),编译器不会进行这种转换序列。
提供到dumb指针的隐式类型转换的灵巧指针类也暴露了一个非常有害的bug。考虑这个代码:
...
delete pt;
这段代码应该不能被编译,pt不是指针,它是一个对象,你不能删除一个对象。只有指针才能被删除,对么?
当然对了。但是回想一下条款5:编译器使用隐式类型转换来尽可能使函数调用成功,再回想一下条款8:使用delete会调用析构函数和operator delete,两者都是函数。编译器欲使在delete语句里的两个函数成功调用,就把pt隐式转换为Tuple*,然后删除它。这样做必然会破坏你的程序。
如果pt拥有它指向的对象,对象就会被删除两次,一次在调用delete时,第二次在pt的析构函数被调用时。如果pt不拥有对象,而是其他人拥有,拥有者可以删除pt,但是如果pt指向对象的拥有者不是删除pt的人,有删除权的拥有者以后还会再次删除该对象。不论是前者所述的情况还是后者的情况都会导致一个对象被删除两次,这样做会产生不能预料的后果。
这个bug极为有害,因为隐藏在灵巧指针后面的全部思想就是让它们不论是在外观上还是在使用感觉上都与dumb指针尽可能地相似。你越接近这种思想,你的客户端就越可能忘记正在使用灵巧指针。如果他们忘记了正在使用灵巧指针,肯定会在调用new之后调用delete,以防止资源泄漏,谁又能责备他们这样做不对呢?
底线很简单:除非有一个让人非常信服的原因去这样做,否则绝对不要提供转换到dumb指针的隐式类型转换操作符。
灵巧指针和继承类到基类的类型转换
假设我们有一个public继承层次结构,以模型化音乐商店的商品:
public:
MusicProduct(const string& title);
virtual void play() const = 0;
virtual void displayTitle() const = 0;
...
} ;
class Cassette: public MusicProduct {
public:
Cassette(const string& title);
virtual void play() const;
virtual void displayTitle() const;
...
} ;
class CD: public MusicProduct {
public:
CD(const string& title);
virtual void play() const;
virtual void displayTitle() const;
...
} ;
再接着假设,我们有一个函数,给它一个MusicProduct对象,它能显示产品名,并播放它:
{
for (int i = 1; i <= numTimes; ++i) {
pmp->displayTitle();
pmp->play();
}
}
这个函数能够这样使用:
CD * nightmareMusic = new CD( " Disco Hits of the 70s " );
displayAndPlay(funMusic, 10 );
displayAndPlay(nightmareMusic, 0 );
这并没有什么值得惊讶的东西,但是当我们用灵巧指针替代dumb指针,会发生什么呢:
int numTimes);
SmartPtr < Cassette > funMusic( new Cassette( " Alapalooza " ));
SmartPtr < CD > nightmareMusic( new CD( " Disco Hits of the 70s " ));
displayAndPlay(funMusic, 10 ); // 错误!
displayAndPlay(nightmareMusic, 0 ); // 错误!
如果灵巧指针这么聪明,为什么不能编译这些代码呢?
不能进行编译原因是不能把SmartPtr<CD>或SmartPtr<Cassette>转换成SmartPtr<MusicProduct>。从编译器的观点来看,这些类之间没有任何关系。为什么编译器的会这样认为呢?毕竟SmartPtr<CD> 或 SmartPtr<Cassette>不是从SmartPtr<MusicProduct>继承过来的,这些类之间没有继承关系,我们不可能要求编译器把一种对象转换成另一种类型的对象。
幸运的是,有办法避开这种限制,这种方法的核心思想(不是实际操作)很简单:对于可以进行隐式转换的每个灵巧指针类都提供一个隐式类型转换操作符(参见条款5)。例如在music类层次内,在Cassette和CD的灵巧指针类内你可以加入SmartPtr<MusicProduct>函数:
public:
operator SmartPtr<MusicProduct>()
{ return SmartPtr<MusicProduct>(pointee); }
...
private:
Cassette *pointee;
} ;
class SmartPtr < CD > {
public:
operator SmartPtr<MusicProduct>()
{ return SmartPtr<MusicProduct>(pointee); }
...
private:
CD *pointee;
} ;
这种方法有两个缺点。第一,你必须人为地特化(specialize)SmartPtr类,所以你加入隐式类型转换操作符也就破坏了模板的通用性。第二,你可能必须添加许多类型转换符,因为你指向的对象可以位于继承层次中很深的位置,你必须为直接或间接继承的每一个基类提供一个类型转换符。(如果你想你能够克服这个缺点,方法是仅仅为转换到直接基类而提供一个隐式类型转换符,那么你再想想这样做行么?因为编译器在同一时间调用用户定义类型转换函数的次数不能超过一次,它们不能把指向T的灵巧指针转换为指向T的间接基类的灵巧指针,除非只要一步就能完成。)
如果你能让编译器为你编写所有的类型转换函数,这会节省很多时间。感谢最近的语言扩展,让你能够做到,这个扩展能声明(非虚)成员函数模板(通常就叫成员模板(member template)),你能使用它来生成灵巧指针类型转换函数,如下:
class SmartPtr { // 灵巧指针
public:
SmartPtr(T* realPtr = 0);
T* operator->() const;
T& operator*() const;
template<class newType> // 模板成员函数
operator SmartPtr<newType>() // 为了实现隐式类型转换。
{
return SmartPtr<newType>(pointee);
}
...
} ;
现在请你注意,这可不是魔术——不过也很接近于魔术。它的原理如下所示。(如果下面的内容让你感到既冗长又令你费解,请不要失望,一会儿我会给出一个例子。我保证你看完例子后,就能够更深入地理解这段内容了)假设编译器有一个指向T对象的灵巧指针,它要把这个对象转换成指向“T的基类”的灵巧指针。编译器首先检查SmartPtr<T>的类定义,看其有没有声明必须的类型转换符,但是它没有声明。(这不是指:在模板上面没有声明类型转换符)编译器然后检查是否存在一个成员函数模板,可以被实例化,用来进行它所期望的类型转换。它发现了一个这样的模板(带有形式类型参数newType),所以它把newType绑定到T的基类类型上,来实例化模板。这时,惟一一个问题是实例化的成员函数代码能否被编译。传递(dumb)指针pointee到指向“T的基类”的灵巧指针的构造函数,这个语句是合法的,把它转变成指向其基类(public 或 protected)对象的指针也必然是合法的,因此类型转换操作符能够被编译,可以成功地把指向T的灵巧指针隐式地类型转换为指向“T的基类”的灵巧指针。
举一个例子会有所帮助。让我们回到CDs、cassettes、music产品的继承层次上来。我们先前已经知道下面这段代码不能被编译,因为编译器不能把指向CD的灵巧指针转换为指向music产品的灵巧指针:
int howMany);
SmartPtr < Cassette > funMusic( new Cassette( " Alapalooza " ));
SmartPtr < CD > nightmareMusic( new CD( " Disco Hits of the 70s " ));
displayAndPlay(funMusic, 10 ); // 以前是一个错误
displayAndPlay(nightmareMusic, 0 ); // 以前是一个错误
修改了灵巧指针类,包含了隐式类型转换操作符的成员函数模板以后,这个代码就可以成功运行了。拿如下调用举例,看看为什么能够成功运行:
funMusic对象的类型是SmartPtr<Cassette>。函数displayAndPlay期望的参数是SmartPtr<Mus-icProduct>地对象。编译器侦测到类型不匹配并寻找把funMusic转换成SmartPtr<MusicProduct>对象的方法。它在SmartPtr<MusicProduct>类里寻找带有SmartPtr<Cassette>类型参数的单参数构造函数(参见条款5),但是没有找到。然后它们又寻找成员函数模板,能被实例化产生这样的函数。它们在SmartPtr<Cassette>发现了模板,把newType绑定到MusicProduct上,生成必须的函数。实例化函数,生成这样的代码:
{
return SmartPtr<MusicProduct>(pointee);
}
能编译这行代码么?实际上这段代码就是用pointee做为参数调用SmartPtr<MusicProduct>的构造函数,所以真正的问题是能否用一个Cassette*指针构造一个SmartPtr<MusicProduct>对象,现在我们对dumb指针类型之间的转换已经很熟悉了,Cassette*能够被传递给需要MusicProduct*指针的地方。因此SmartPtr<MusicProduct>构造函数可以成功调用,同样SmartPtr<Cassette>到SmartPtr <MusicProduct>之间的类型转换也能成功进行。太棒了,实现了灵巧指针之间的类型转换,还有什么比这更简单么?
而且,还有什么比这功能更强大么?不要被这个例子误导,而认为这种方法只能用于把指针在继承层次中向上进行类型转换。这种方法可以成功地用于任何合法的指针类型转换。如果你有dumb指针T1*和另一种dumb指针T2*,当且仅当你能隐式地把T1*转换为T2*时,你就能够隐式地把指向T1的灵巧指针类型转换为指向T2的灵巧指针类型。
这种技术能给我们几乎所有想要的行为特性。假设我们用一个新类CasSingle来扩充MusicProd-uct类层次,用来表示cassette singles。修改后的类层次看起来象这样:
现在考虑这段代码:
class SmartPtr { ... } ; // 转换操作符的成员模板
void displayAndPlay( const SmartPtr < MusicProduct >& pmp,
int howMany);
void displayAndPlay( const SmartPtr < Cassette >& pc,
int howMany);
SmartPtr < CasSingle > dumbMusic( new CasSingle( " Achy Breaky Heart " ));
displayAndPlay(dumbMusic, 1 ); // 错误!
在这个例子里,displayAndPlay被重载,一个函数带有SmartPtr<Cassette> 对象参数,其它函数的参数为SmartPtr<CasSingle>,我们期望调用SmartPtr<Cassette>,因为CasSingle是直接从Cassette上继承下来的,而它仅仅是间接继承自MusicProduct。当然这是dumb指针的工作方法,我们的灵巧指针不会这么灵巧。它们把成员函数做为转换操作符来使用,就C++编译器而言,所有类型转换操作符都一样,没有好坏的分别。因此displayAndPlay的调用具有二义性,因为从SmartPtr<C-asSingle> 到SmartPtr<Cassette>的类型转换并不比到SmartPtr<MusicProduct>的类型转换好。
通过成员模板来实现灵巧指针的类型转换有还有两个缺点。第一,支持成员模板的编译器较少,所以这种技术不具有可移植性。以后情况会有所改观,但是没有人知道这会等到什么时候。第二,这种方法的工作原理不很明了,要理解它必须先要深入理解函数调用的参数匹配,隐式类型转换函数,模板函数隐式实例化和成员函数模板。有些程序员以前从来没有看到过这种技巧,而却被要求维护使用这种技巧的代码,我真是很可怜他们。这种技巧确实很巧妙,这自然是肯定,但是过于的巧妙可是一件危险的事情。
不要再拐弯抹角了,直接了当地说,我们想要知道的是在继承类向基类进行类型转换方面,我们如何能够让灵巧指针的行为与dumb指针一样呢?答案很简单:不可能。正如Daniel Edelson所说,灵巧指针固然灵巧,但不是指针。最好的方法是使用成员模板生成类型转换函数,在会产生二义性结果的地方使用casts(参见条款2)。这不是一个完美的方法,不过也很不错,在一些情况下去除二义性,所付出的代价与灵巧指针提供复杂的功能相比还是值得的。
灵巧指针和const
对于dumb指针来说,const既可以针对指针所指向的东西,也可以针对于指针本身,或者兼有两者的含义(参见Effective C++条款21):
const CD * p; // p 是一个non-const 指针
// 指向 const CD 对象
CD * const p = & goodCD; // p 是一个const 指针
// 指向non-const CD 对象;
// 因为 p 是const, 它
// 必须被初始化
const CD * const p = & goodCD; // p 是一个const 指针
// 指向一个 const CD 对象
我们自然想要让灵巧指针具有同样的灵活性。不幸的是只能在一个地方放置const,并只能对指针本身起作用,而不能针对于所指对象:
& goodCD; // 指向 non-const CD 对象
好像有一个简单的补救方法,就是建立一个指向cosnt CD的灵巧指针:
& goodCD; // 指向const CD 对象
现在我们可以建立const和non-const对象和指针的四种不同组合:
// non-const 指针
SmartPtr < const CD > p; // const 对象,
// non-const 指针
const SmartPtr < CD > p = & goodCD; // non-const 对象
// const指针
const SmartPtr < const CD > p = & goodCD; // const 对象
// const 指针
但是美中不足的是,使用dumb指针我们能够用non-const指针初始化const指针,我们也能用指向non-cosnt对象的指针初始化指向const对象的指针;就像进行赋值一样。例如:
const CD * pConstCD = pCD; // 正确
但是如果我们试图把这种方法用在灵巧指针上,情况会怎么样呢?
SmartPtr < const CD > pConstCD = pCD; // 正确么?
SmartPtr<CD> 与SmartPtr<const CD>是完全不同的类型。在编译器看来,它们是毫不相关的,所以没有理由相信它们是赋值兼容的。到目前为止这是一个老问题了,把它们变成赋值兼容的惟一方法是你必须提供函数,用来把SmartPtr<CD>类型的对象转换成SmartPtr<const CD>类型。如果你使用的编译器支持成员模板,就可以利用前面所说的技巧自动生成你需要的隐式类型转换操作。(我前面说过,只要对应的dumb指针能进行类型转换,灵巧指针也就能进行类型转换,我没有欺骗你们。包含const类型转换也没有问题。)如果你没有这样的编译器,你必须克服更大的困难。
包括const的类型转换是单向的:从non-const到const的转换是安全的,但是从const到non-const则不是安全的。而且用const指针能的事情,用non-const指针也能做,但是用non-const指针还能做其它一些事情(例如,赋值操作)。同样,用指向const的指针能做的任何事情,用指向non-const的指针也能做到,但是用指向non-const的指针能够完成一些使用指向const的指针所不能完成的事情(例如,赋值操作)。
这些规则看起来与public继承的规则相类似(Effective C++ 条款35)。你能够把一个派生类对象转换成基类对象,但是反之则不是这样,你对基类所做的任何事情对派生类也能做,但是还能对派生类做另外一些事情。我们能够利用这一点来实作灵巧指针,就是说可以让每个指向T的灵巧指针类public派生自一个对应的指向const-T的灵巧指针类:
class SmartPtrToConst { // 灵巧指针
... // 灵巧指针通常的
// 成员函数
protected:
union {
const T* constPointee; // 让 SmartPtrToConst 访问
T* pointee; // 让 SmartPtr 访问
};
} ;
template < class T > // 指向non-const对象
class SmartPtr: // 的灵巧指针
public SmartPtrToConst < T > {
... // 没有数据成员
} ;
使用这种设计方法,指向non-const-T对象的灵巧指针包含一个指向const-T的dumb指针,指向const-T的灵巧指针需要包含一个指向cosnt-T的dumb指针。最方便的方法是把指向const-T的dumb指针放在基类里,把指向non-const-T的dumb指针放在派生类里,然而这样做有些浪费,因为SmartPtr对象包含两个dumb指针:一个是从SmartPtrToConst继承来的,一个是SmartPtr自己的。
一种在C世界里的老式武器可以解决这个问题,这就是union,它在C++中同样有用。Union在protected中,所以两个类都可以访问它,它包含两个必须的dumb指针类型,SmartPtrToConst<T>对象使用constPointee指针,SmartPtr<T>对象使用pointee指针。因此我们可以在不分配额外空间的情况下,使用两个不同的指针(参见Effective C++条款10中另外一个例子)这就是union美丽的地方。当然两个类的成员函数必须约束它们自己仅仅使用适合的指针。这是使用union所冒的风险。
利用这种新设计,我们能够获得所要的行为特性:
SmartPtrToConst < CD > pConstCD = pCD; // 正确
评价
有关灵巧指针的讨论该结束了,在我们离开这个话题之前,应该问这样一个问题:灵巧指针如此繁琐麻烦,是否值得使用,特别是如果你的编译器缺乏支持成员函数模板时。
经常是值得的。例如通过使用灵巧指针极大地简化了条款29中的引用计数代码。而且正如该例子所显示的,灵巧指针的使用在一些领域受到极大的限制,例如测试空值、转换到dumb指针、继承类向基类转换和对指向const的指针的支持。同时灵巧指针的实作、理解和维护需要大量的技巧。Debug使用灵巧指针的代码也比Debug使用dumb指针的代码困难。无论如何你也不可能设计出一种通用目的的灵巧指针,能够替代dumb指针。
达到同样的代码效果,使用灵巧指针更方便。灵巧指针应该谨慎使用, 不过每个C++程序员最终都会发现它们是有用的。