条款28:灵巧(smart)指针(中)
测试灵巧指针是否为NULL
目前为止我们讨论的函数能让我们建立、释放、拷贝、赋值、dereference灵巧指针。但是有一件我们做不到的事情是“发现灵巧指针为NULL”:
SmartPtr<TreeNode> ptn;
...
if (ptn == 0) ... // error!
if (ptn) ... // error!
if (!ptn) ... // error!
这是一个严重的限制。
在灵巧指针类里加入一个isNull成员函数是一件很容易的事,但是仍然没有解决当测试NULL时灵巧指针的行为与dumb pointer不相似的问题。另一种方法是提供隐式类型转换操作符,允许编译上述的测试。一般应用于这种目的的类型转换是void* :
template<class T>
class SmartPtr {
public:
...
operator void*(); // 如果灵巧指针为null,
... // 返回0,否则返回
}; // 非0。
SmartPtr<TreeNode> ptn;
...
if (ptn == 0) ... // 现在正确
if (ptn) ... // 也正确
if (!ptn) ... // 正确
这与iostream类中提供的类型转换相同,所以可以这样编写代码:
ifstream inputFile("datafile.dat");
if (inputFile) ... // 测试inputFile是否已经被
// 成功地打开。
象所有的类型转换函数一样,它有一个缺点,在一些情况下虽然大多数程序员希望它调用失败,但是函数还能够成功地被调用(参见条款5)。特别是它允许灵巧指针与完全不同的类型之间进行比较:
SmartPtr<Apple> pa;
SmartPtr<Orange> po;
...
if (pa == po) ... // 这能够被成功编译!
即使在SmartPtr<Apple> 和 SmartPtr<Orange>之间没有operator= 函数,也能够编译,因为灵巧指针被隐式地转换为void*指针,对于内建指针类型有一个内建的比较函数。这种进行隐式类型转换的行为特性很危险。(再看一下条款5,必须反反复复地阅读,做到耳熟能详。)
在void*类型转换方面,也有一些变通之策。有些设计者采用到const void*的类型转换,还有一些采取转换到bool的方法。这些变通之策都没有消除混合类型比较的问题。
有一种两全之策可以提供合理的测试空值的语法形式,同时把不同类型的灵巧指针之间进行比较的可能性降到最低。这就是在灵巧指针类中重载operator!,当且仅当灵巧指针是一个空指针时,operator!返回true:
template<class T>
class SmartPtr {
public:
...
bool operator!() const; // 当且仅当灵巧指针是
... // 空值,返回true。
};
客户端程序如下所示:
SmartPtr<TreeNode> ptn;
...
if (!ptn) { // 正确
... // ptn 是空值
}
else {
... // ptn不是空值
}
但是这样就不正确了:
if (ptn == 0) ... // 仍然错误
if (ptn) ... // 也是错误的
仅在这种情况下会存在不同类型之间进行比较:
SmartPtr<Apple> pa;
SmartPtr<Orange> po;
...
if (!pa == !po) ... // 能够编译
幸好程序员不会经常这样编写代码。有趣的是,iostream库的实作除了提供void*隐式的类型转换,也有operator!函数,不过这两个函数被用于测试的流状态有些不同。(在C++类库标准中(参见Effective C++ 条款49和本书条款35),void*隐式的类型转换已经被bool类型的转换所替代,operator bool总是返回与operator!相反的值。)
把灵巧指针转变成dumb指针
有时你要在一个程序里或已经使用dumb指针的程序库中添加灵巧指针。例如,你的分布式数据库系统原来不是分布式的,所以可能有一些老式的库函数没有使用灵巧指针:
class Tuple { ... }; // 同上
void normalize(Tuple *pt); // 把*pt 放入
// 范式中; 注意使用的
// 是dumb指针
考虑一下,如果你试图用指向Tuple的灵巧指针调用normalize,会出现什么情况:
DBPtr<Tuple> pt;
...
normalize(pt); // 错误!
这种调用不能够编译,因为不能把DBPtr<Tuple>转换成Tuple*。你可以这样做,从而使该该函数正常运行:
normalize(&*pt); // 繁琐, 但合法
不过我觉得你会讨厌这种调用方式。
在灵巧指针模板中增加指向T的dumb指针的隐式类型转换操作符,可以让以上函数调用成功运行:
template<class T> // 同上
class DBPtr {
public:
...
operator T*() { return pointee; }
...
};
DBPtr<Tuple> pt;
...
normalize(pt); // 能够运行
并且这个函数也消除了测试空值的问题:
if (pt == 0) ... // 正确, 把pt转变成
// Tuple*
if (pt) ... // 同上
if (!pt) ... // 同上 (reprise)
然而,它也有类型转换函数所具有的缺点(几乎总是这样,看条款5)。它使得客户端能够很容易地直接访问dumb指针,绕过“类指针(pointer-like)”对象所提供的“灵巧”特性:
void processTuple(DBPtr<Tuple>& pt)
{
Tuple *rawTuplePtr = pt; // 把DBPtr<Tuple> 转变成
// Tuple*
使用raw TuplePtr 修改 tuple;
}
通常,灵巧指针提供的“灵巧”行为特性是设计中的主要组成部分,所以允许客户端使用dumb指针会导致灾难性的后果。例如,如果DBPtr实现了条款29中引用计数的功能,允许客户端直接对dumb指针进行操作很可能破坏“引用计数”数据结构,而导致引用计数错误。
甚至即使你提供一个从灵巧指针到dumb指针的隐式转换操作符,灵巧指针也不能真正地做到与dumb指针互换。因为从灵巧指针到dumb指针的转换是“用户定义类型转换”,在同一时间编译器进行这种转换的次数不能超过一次。例如假设有一个表示能够访问某一元组的所有客户的类:
class TupleAccessors {
public:
TupleAccessors(const Tuple *pt); // pt identifies the
... // tuple whose accessors
}; // we care about
通常,TupleAccessors的单参数构造函数也可以做为从Tuple*到TupleAccessors的类型转换操作符(参见条款5)。现在考虑一下用于合并两个TupleAccessors对象内信息的函数:
TupleAccessors merge(const TupleAccessors& ta1,
const TupleAccessors& ta2);
因为一个Tuple*可以被隐式地转换为TupleAccessors,用两个dumb Tuple*调用merge函数,可以正常运行:
Tuple *pt1, *pt2;
...
merge(pt1, pt2); // 正确, 两个指针被转换为
// TupleAccessors objects
如果用灵巧指针DBPtr<Tuple>进行调用,编译就会失败:
DBPtr<Tuple> pt1, pt2;
...
merge(pt1, pt2); // 错误!不能把 pt1 和
// pt2转换称TupleAccessors对象
因为从DBPtr<Tuple>到TupleAccessors的转换要调用两次用户定义类型转换(一次从DBPtr<Tuple>到Tuple*,一次从Tuple*到TupleAccessors),编译器不会进行这种转换序列。
提供到dumb指针的隐式类型转换的灵巧指针类也暴露了一个非常有害的bug。考虑这个代码:
DBPtr<Tuple> pt = new Tuple;
...
delete pt;
这段代码应该不能被编译,pt不是指针,它是一个对象,你不能删除一个对象。只有指针才能被删除,对么?
当然对了。但是回想一下条款5:编译器使用隐式类型转换来尽可能使函数调用成功,再回想一下条款8:使用delete会调用析构函数和operator delete,两者都是函数。编译器欲使在delete语句里的两个函数成功调用,就把pt隐式转换为Tuple*,然后删除它。这样做必然会破坏你的程序。
如果pt拥有它指向的对象,对象就会被删除两次,一次在调用delete时,第二次在pt的析构函数被调用时。如果pt不拥有对象,而是其他人拥有,拥有者可以删除pt,但是如果pt指向对象的拥有者不是删除pt的人,有删除权的拥有者以后还会再次删除该对象。不论是前者所述的情况还是后者的情况都会导致一个对象被删除两次,这样做会产生不能预料的后果。
这个bug极为有害,因为隐藏在灵巧指针后面的全部思想就是让它们不论是在外观上还是在使用感觉上都与dumb指针尽可能地相似。你越接近这种思想,你的客户端就越可能忘记正在使用灵巧指针。如果他们忘记了正在使用灵巧指针,肯定会在调用new之后调用delete,以防止资源泄漏,谁又能责备他们这样做不对呢?
底线很简单:除非有一个让人非常信服的原因去这样做,否则绝对不要提供转换到dumb指针的隐式类型转换操作符。
灵巧指针和继承类到基类的类型转换
假设我们有一个public继承层次结构,以模型化音乐商店的商品:
class MusicProduct {
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对象,它能显示产品名,并播放它:
void displayAndPlay(const MusicProduct* pmp, int numTimes)
{
for (int i = 1; i <= numTimes; ++i) {
pmp->displayTitle();
pmp->play();
}
}
这个函数能够这样使用:
Cassette *funMusic = new Cassette("Alapalooza");
CD *nightmareMusic = new CD("Disco Hits of the 70s");
displayAndPlay(funMusic, 10);
displayAndPlay(nightmareMusic, 0);
这并没有什么值得惊讶的东西,但是当我们用灵巧指针替代dumb指针,会发生什么呢:
void displayAndPlay(const SmartPtr<MusicProduct>& pmp,
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>函数:
class SmartPtr<Cassette> {
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)),你能使用它来生成灵巧指针类型转换函数,如下:
template<class T> // 模板类,指向T的
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产品的灵巧指针:
void displayAndPlay(const SmartPtr<MusicProduct>& pmp,
int howMany);
SmartPtr<Cassette> funMusic(new Cassette("Alapalooza"));
SmartPtr<CD> nightmareMusic(new CD("Disco Hits of the 70s"));
displayAndPlay(funMusic, 10); // 以前是一个错误
displayAndPlay(nightmareMusic, 0); // 以前是一个错误
修改了灵巧指针类,包含了隐式类型转换操作符的成员函数模板以后,这个代码就可以成功运行了。拿如下调用举例,看看为什么能够成功运行:
displayAndPlay(funMusic, 10);
funMusic对象的类型是SmartPtr<Cassette>。函数displayAndPlay期望的参数是SmartPtr<MusicProduct>地对象。编译器侦测到类型不匹配并寻找把funMusic转换成SmartPtr<MusicProduct>对象的方法。它在SmartPtr<MusicProduct>类里寻找带有SmartPtr<Cassette>类型参数的单参数构造函数(参见条款5),但是没有找到。然后它们又寻找成员函数模板,能被实例化产生这样的函数。它们在SmartPtr<Cassette>发现了模板,把newType绑定到MusicProduct上,生成必须的函数。实例化函数,生成这样的代码:
SmartPtr<Cassette>:: operator SmartPtr<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的灵巧指针类型。