假设我们有一个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>继承过来的,这些类之间没有继承关系,我们不可能要求编译器把一种对象转换成(完全不同的)另一种类型的对象。
幸运的是,有办法避开这种限制,这种方法的核心思想(不是实际操作)很简单:对于
可以进行隐式转换的每个灵巧指针类型都提供一个隐式类型转换操作符如果你能让编译器为你编写所有的类型转换函数,这会节省很多时间。感谢最近的语言扩展,让你能够做到,这个扩展能声明(非虚)成员函数模板(通常就叫成员模板(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的基类”的灵巧指针的构造函数,必须合法的。指针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>类型参数的单参数构造函数 ,但是没有找到。然后它们又寻找成员函数模板,以实例化产生这样的函数。它们在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的灵巧指针类型。
现在考虑这段代码:
template<class T> // 同上, 包括作为类型
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<CasSingle> 到SmartPtr<Cassette>的类型转换并不比到SmartPtr<MusicProduct>的类型转换优先。
通过成员模板来实现灵巧指针的类型转换还有两个缺点。第一,支持成员模板的编译器
较少,所以这种技术不具有可移植性。以后情况会有所改观,但是没有人知道这会等到什么时候。第二,这种方法的工作原理不很明了,要理解它必须先要深入理解函数调用时的参数匹配,隐式类型转换函数,模板函数隐式实例化和成员函数模板。有些程序员以前从来没有看到过这种技巧,而却被要求维护使用了这种技巧的代码,我真是很可怜他们。这种技巧确实很巧妙,这自然是肯定,但是过于的巧妙可是一件危险的事情。
不要再拐弯抹角了,直接了当地说,我们想要知道的是在继承类向基类进行类型转换方
面, 我们如何能够让灵巧指针的行为与dumb指针一样呢?答案很简单: 不可能。 正如Daniel Edelson所说,灵巧指针固然灵巧,但不是指针。最好的方法是使用成员模板生成类型转换函数,在会产生二义性结果的地方使用casts(类型转换) 。这不是一个完美的方法,不过已经很不错了,在一些情况下需去除二义性,所付出的代价与灵巧指针提供复杂的功能相比还是值得的。