Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1791279
  • 博文数量: 600
  • 博客积分: 10581
  • 博客等级: 上将
  • 技术积分: 6205
  • 用 户 组: 普通用户
  • 注册时间: 2008-11-06 10:13
文章分类
文章存档

2016年(2)

2015年(9)

2014年(8)

2013年(5)

2012年(8)

2011年(36)

2010年(34)

2009年(451)

2008年(47)

分类: C/C++

2009-08-26 15:24:25

通常,灵巧指针提供的“灵巧”行为特性是设计中的主要组成部分,所以允许用户使用dumb指针会导致灾难性的后果。例如,如果DBPtr实现了条款M29中引用计数的功能,允许客户端直接对dumb指针进行操作很可能破坏“引用计数”数据结构,而导致引用计数错误。

甚至即使你提供一个从灵巧指针到dumb指针的隐式转换操作符,灵巧指针也不能真正地做到与dumb指针互换。因为从灵巧指针到dumb指针的转换是“用户定义类型转换”,在同一时间编译器进行这种转换的次数不能超过一次。例如假设有一个表示所有能够访问某一元组的用户的类:

class TupleAccessors {

public:

  TupleAccessors(const Tuple *pt);   // pt identifies the

  ...                                // tuple whose accessors

};                                   // we care about

通常,TupleAccessors的单参数构造函数也可以作为从Tuple*TupleAccessors的类型转换操作符(参见条款M5)。现在考虑一下用于合并两个TupleAccessors对象内信息的函数:

TupleAccessors merge(const TupleAccessors& ta1,

                     const TupleAccessors& ta2);

因为一个Tuple*可以被隐式地转换为TupleAccessors,用两个dumb Tuple*调用merge函数,可以正常运行:

Tuple *pt1, *pt2;

...

merge(pt1, pt2);                     // 正确, 两个指针被转换为

                                     // TupleAccessors objects

如果用灵巧指针DBPtr进行调用,编译就会失败:

DBPtr pt1, pt2;

...

merge(pt1, pt2);                 // 错误!不能把 pt1

                                 // pt2转换称TupleAccessors对象

因为从DBPtrTupleAccessors的转换要调用两次用户定义类型转换(一次从DBPtrTuple*,一次从Tuple*TupleAccessors),编译器不会进行这种序列的转换。

提供到dumb指针的隐式类型转换的灵巧指针类也暴露了一个非常有害的bug。考虑这个代码:

DBPtr pt = new Tuple;

...

delete pt;

这段代码应该不能被编译,pt不是指针,它是一个对象,你不能删除一个对象。只有指针才能被删除,对么?

当然对了。但是回想一下条款M5:编译器使用隐式类型转换来尽可能使函数调用成功;再回想一下条款M8:使用delete会调用析构函数和operator delete,两者都是函数。编译器为了使在delete pt语句里的两个函数成功调用,就把pt隐式转换为Tuple*,然后删除它。(本来是你写错了代码,而现在却编译过了,)这样做必然会破坏你的程序。

如果pt拥有它指向的对象,对象就会被删除两次,一次在调用delete时,第二次在pt的析构函数被调用时。当pt不拥有对象,而是其它人拥有时,如果拥有者同时负责删除pt的则情况还好,但是如果拥有者不是负责删除pt的人,可以预料它以后还会再次删除该对象。不论是最前者所述的情况还是最后者的情况都会导致一个对象被删除两次,这样做会产生不能预料的后果。

这个bug极为有害,因为隐藏在灵巧指针后面的全部思想就是让它们不论是在外观上还是在使用感觉上都与dumb指针尽可能地相似。你越接近这种思想,你的用户就越可能忘记正在使用灵巧指针。如果他们忘记了正在使用灵巧指针,肯定会在调用new之后调用delete,以防止资源泄漏,谁又能责备他们这样做不对呢?

底线很简单:除非有一个让人非常信服的原因去这样做,否则绝对不要提供转换到dumb指针的隐式类型转换操作符。

l         灵巧指针和继承类到基类的类型转换

假设我们有一个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& pmp,

                    int numTimes);

SmartPtr funMusic(new Cassette("Alapalooza"));

SmartPtr nightmareMusic(new CD("Disco Hits of the 70s"));

displayAndPlay(funMusic, 10);         // 错误!

displayAndPlay(nightmareMusic, 0);    // 错误!

既然灵巧指针这么聪明,为什么不能编译这些代码呢?

不能进行编译的原因是不能把SmartPtrSmartPtr转换成SmartPtr。从编译器的观点来看,这些类之间没有任何关系。为什么编译器的会这样认为呢?毕竟SmartPtr SmartPtr不是从SmartPtr继承过来的,这些类之间没有继承关系,我们不可能要求编译器把一种对象转换成(完全不同的)另一种类型的对象。

幸运的是,有办法避开这种限制,这种方法的核心思想(不是实际操作)很简单:对于可以进行隐式转换的每个灵巧指针类型都提供一个隐式类型转换操作符(参见条款M5)。例如在music类层次内,在CassetteCD的灵巧指针类内你可以加入operator SmartPtr函数:

class SmartPtr {

public:

  operator SmartPtr()

  { return SmartPtr(pointee); }

  ...

private:

  Cassette *pointee;

};

class SmartPtr {

public:

  operator SmartPtr()

  { return SmartPtr(pointee); }

  ...

private:

  CD *pointee;

};

这种方法有两个缺点。第一,你必须人为地特化(specializeSmartPtr类,所以你加入隐式类型转换操作符也就破坏了模板的通用性。第二,你可能必须添加许多类型转换符,因为你指向的对象可以位于继承层次中很深的位置,你必须为直接或间接继承的每一个基类提供一个类型转换符。(如果你认为你能够克服这个缺点,方法是仅仅为转换到直接基类而提供一个隐式类型转换符,那么请再想想这样做行么?因为编译器在同一时间调用用户定义类型转换函数的次数不能超过一次,它们不能把指向T的灵巧指针转换为指向T的间接基类的灵巧指针,除非只要一步就能完成。)

如果你能让编译器为你编写所有的类型转换函数,这会节省很多时间。感谢最近的语言扩展,让你能够做到,这个扩展能声明(非虚)成员函数模板(通常就叫成员模板(member template)),你能使用它来生成灵巧指针类型转换函数,如下:

template                    // 模板类,指向T

class SmartPtr {                     // 灵巧指针

public:

  SmartPtr(T* realPtr = 0);

  T* operator->() const;

  T& operator*() const;

  template             // 模板成员函数

  operator SmartPtr()        // 为了实现隐式类型转换.

  {

    return SmartPtr(pointee);

  }

  ...

};

现在请你注意,这可不是魔术——不过也很接近于魔术。它的原理如下所示。(如果下面的内容让你感到既冗长又令你费解,请不要失望,一会儿我会给出一个例子。我保证你看完例子后,就能够更深入地理解这段内容了。)假设编译器有一个指向T对象的灵巧指针,它要把这个对象转换成指向“T的基类”的灵巧指针。编译器首先检查SmartPtr的类定义,看其有没有声明明确的类型转换符,但是它没有声明。(这不是指:在上面的模板没有声明类型转换符。)编译器然后检查是否存在一个成员函数模板,并可以被实例化成它所期望的类型转换。它发现了一个这样的模板(带有形式类型参数newType),所以它把newType绑定成T的基类类型来实例化模板。这时,惟一的问题是实例化的成员函数代码能否被编译:传递(dumb)指针pointee到指向“T的基类的灵巧指针的构造函数,必须合法的。指针pointee是指向T类型的,把它转变成指向其基类(public protected)对象的指针必然是合法的,因此类型转换操作符能够被编译,可以成功地把指向T的灵巧指针隐式地类型转换为指向“T的基类”的灵巧指针。

举一个例子会有所帮助。让我们回到CDscassettesmusic产品的继承层次上来。我们先前已经知道下面这段代码不能被编译,因为编译器不能把指向CD的灵巧指针转换为指向music产品的灵巧指针:

void displayAndPlay(const SmartPtr& pmp,

                    int howMany);

SmartPtr funMusic(new Cassette("Alapalooza"));

SmartPtr nightmareMusic(new CD("Disco Hits of the 70s"));

displayAndPlay(funMusic, 10);         // 以前是一个错误

displayAndPlay(nightmareMusic, 0);    // 以前是一个错误

修改了灵巧指针类,包含了隐式类型转换操作符的成员函数模板以后,这个代码就可以成功运行了。拿如下调用举例,看看为什么能够成功运行:

displayAndPlay(funMusic, 10);

funMusic对象的类型是SmartPtr。函数displayAndPlay期望的参数是SmartPtr地对象。编译器侦测到类型不匹配,于是寻找把funMusic转换成SmartPtr对象的方法。它在SmartPtr类里寻找带有SmartPtr类型参数的单参数构造函数(参见条款M5),但是没有找到。然后它们又寻找成员函数模板,以实例化产生这样的函数。它们在SmartPtr发现了模板,把newType绑定到MusicProduct上,生成了所需的函数。实例化函数,生成这样的代码:

SmartPtr::  operator SmartPtr()

{

  return SmartPtr(pointee);

}

能编译这行代码么?实际上这段代码就是用pointee做为参数调用SmartPtr的构造函数,所以真正的问题是能否用一个Cassette*指针构造一个SmartPtr对象。现在我们对dumb指针类型之间的转换已经很熟悉了,Cassette*能够被传递给需要MusicProduct*指针的地方。因此SmartPtr构造函数可以成功调用,同样SmartPtrSmartPtr之间的类型转换也能成功进行。太棒了,实现了灵巧指针之间的类型转换,还有什么比这更简单么?

 此外,还有其它功能么?不要被这个例子误导,而认为这种方法只能用于把指针在继承层次中向上进行类型转换。这种方法可以成功地用于任何合法的指针类型转换。如果你有dumb指针T1*和另一种dumb指针T2*,当且仅当你能隐式地把T1*转换为T2*时,你就能够隐式地把指向T1的灵巧指针类型转换为指向T2的灵巧指针类型。

这种技术能给我们几乎所有想要的行为特性。假设我们用一个新类CasSingle来扩充MusicProduct类层次,用来表示cassette singles。修改后的类层次看起来象这样:

现在考虑这段代码:

template                    // 同上, 包括作为类型
class SmartPtr { ... };              // 转换操作符的成员模板
void displayAndPlay(const SmartPtr& pmp,
                    int howMany);
void displayAndPlay(const SmartPtr& pc,
                    int howMany);
SmartPtr dumbMusic(new CasSingle("Achy Breaky Heart"));
displayAndPlay(dumbMusic, 1);        // 错误!

在这个例子里,displayAndPlay被重载,一个函数带有SmartPtr 对象参数,其它函数的参数为SmartPtr,我们期望调用SmartPtr,因为CasSingle是直接从Cassette上继承下来的,而间接继承自MusicProduct。当然这是dumb指针时的工作方法。我们的灵巧指针不会这么灵巧,它们的转换操作符是成员函数,对C++编译器而言,所有类型转换操作符是同等地位的。因此displayAndPlay的调用具有二义性,因为从SmartPtr SmartPtr的类型转换并不比到SmartPtr的类型转换优先。

通过成员模板来实现灵巧指针的类型转换还有两个缺点。第一,支持成员模板的编译器较少,所以这种技术不具有可移植性。以后情况会有所改观,但是没有人知道这会等到什么时候。第二,这种方法的工作原理不很明了,要理解它必须先要深入理解函数调用时的参数匹配,隐式类型转换函数,模板函数隐式实例化和成员函数模板。有些程序员以前从来没有看到过这种技巧,而却被要求维护使用了这种技巧的代码,我真是很可怜他们。这种技巧确实很巧妙,这自然是肯定,但是过于的巧妙可是一件危险的事情。

不要再拐弯抹角了,直接了当地说,我们想要知道的是在继承类向基类进行类型转换方面,我们如何能够让灵巧指针的行为与dumb指针一样呢?答案很简单:不可能。正如Daniel Edelson所说,灵巧指针固然灵巧,但不是指针。最好的方法是使用成员模板生成类型转换函数,在会产生二义性结果的地方使用casts(类型转换,参见条款M2)。这不是一个完美的方法,不过已经很不错了,在一些情况下需去除二义性,所付出的代价与灵巧指针提供复杂的功能相比还是值得的。

l         灵巧指针和const

对于dumb指针来说,const既可以针对指针所指向的东西,也可以针对于指针本身,或者兼有两者的含义(参见Effective C++条款21):

CD goodCD("Flood");
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,并只能对指针本身起作用,而不能针对于所指对象:

const SmartPtr p =                // p 是一个const 灵巧指针

  &goodCD;                             // 指向 non-const CD 对象

好像有一个简单的补救方法,就是建立一个指向cosnt CD的灵巧指针:

SmartPtr p =            // p 是一个 non-const 灵巧指针
  &goodCD;                        // 指向const CD 对象
现在我们可以建立constnon-const对象和指针的四种不同组合:
SmartPtr p;                          // non-const 对象
                                         // non-const 指针
SmartPtr p;                    // const 对象,
                                         // non-const 指针
const SmartPtr p = &goodCD;          // non-const 对象
                                         // const指针
const SmartPtr p = &goodCD;    // const 对象
                                         // const 指针

但是美中不足的是,使用dumb指针我们能够用non-const指针初始化const指针,我们也能用指向non-cosnt对象的指针初始化指向const对象的指针;就像进行赋值一样。例如:

CD *pCD = new CD("Famous Movie Themes");
const CD * pConstCD = pCD;               // 正确

但是如果我们试图把这种方法用在灵巧指针上,情况会怎么样呢?

SmartPtr pCD = new CD("Famous Movie Themes");
SmartPtr pConstCD = pCD;       // 正确么?

SmartPtr SmartPtr CD>是完全不同的类型。在编译器看来,它们是毫不相关的,所以没有理由相信它们是赋值兼容的。到目前为止这是一个老问题了,把它们变成赋值兼容的唯一方法是你必须提供函数,用来把SmartPtr类型的对象转换成SmartPtr CD>类型。如果你使用的编译器支持成员模板,就可以利用前面所说的技巧自动生成你需要的隐式类型转换操作。(我前面说过,只要对应的dumb指针能进行类型转换,灵巧指针也就能进行类型转换,我没有欺骗你们。带const的类型转换也没有问题。)如果你没有这样的编译器,你必须克服更大的困难。

const的类型转换是单向的:从non-constconst的转换是安全的,但是从constnon-const则不是安全的。而且用const指针能做的事情,用non-const指针也能做,但是用non-const指针还能做其它一些事情(例如,赋值操作)。同样,用指向const对象的指针能做的任何事情,用指向non-const对象的指针也能做到,但是用指向non-const对象的指针能够完成一些指向const对象的指针所不能完成的事情(例如,赋值操作)。

这些规则看起来与public继承的规则相类似(Effective C++ 条款35)。你能够把一个派生类对象转换成基类对象,但是反之则不是这样,你对基类所做的任何事情对派生类也能做,但是还能对派生类做另外一些事情。我们能够利用这一点来实现灵巧指针,就是说可以让每个指向T的灵巧指针类public派生自一个对应的指向const-T的灵巧指针类:

template                    // 指向const对象的
class SmartPtrToConst {              // 灵巧指针
  ...                                // 灵巧指针通常的
                                     // 成员函数
protected:
  union {
    const T* constPointee;           //  SmartPtrToConst 访问
    T* pointee;                      //  SmartPtr 访问
  };
};
template                    // 指向non-const对象
class SmartPtr:                      // 的灵巧指针
  public SmartPtrToConst {
  ...                                // 没有数据成员
};

使用这种设计方法,指向non-const-T对象的灵巧指针包含一个指向const-Tdumb指针,指向const-T的灵巧指针需要包含一个指向cosnt-Tdumb指针。最方便的方法是把指向const-Tdumb指针放在基类里,把指向non-const-Tdumb指针放在派生类里,然而这样做有些浪费,因为SmartPtr对象包含两个dumb指针:一个是从SmartPtrToConst继承来的,一个是SmartPtr自己的。

一种在C世界里的老式武器可以解决这个问题,这就是union,它在C++中同样有用。Unionprotected中,所以两个类都可以访问它,它包含两个必须的dumb指针类型,SmartPtrToConst对象使用constPointee指针,SmartPtr对象使用pointee指针。因此我们可以在不分配额外空间的情况下,使用两个不同的指针(参见Effective C++条款10中另外一个例子)这就是union美丽的地方。当然两个类的成员函数必须约束它们自己仅仅使用适合的指针。这是使用union所冒的风险。

利用这种新设计,我们能够获得所要的行为特性:

SmartPtr pCD = new CD("Famous Movie Themes");
SmartPtrToConst pConstCD = pCD;     // 正确

l         评价

有关灵巧指针的讨论该结束了,在我们离开这个话题之前,应该问这样一个问题:灵巧指针如此繁琐麻烦,是否值得使用,特别是如果你的编译器缺乏支持成员函数模板时。

通常是值得的。例如通过使用灵巧指针极大地简化了条款M29中的引用计数代码。而且正如该例子所显示的,灵巧指针的使用在一些领域受到极大的限制,例如测试空值、转换到dumb指针、继承类向基类转换和对指向const的指针的支持。同时灵巧指针的实现、理解和维护需要大量的技巧。调试使用灵巧指针的代码也比调试使用dumb指针的代码困难。无论如何你也不可能设计出一种通用目的的灵巧指针,能够替代dumb指针。

达到同样的代码效果,使用灵巧指针更方便。灵巧指针应该谨慎使用, 不过每个C++程序员最终都会发现它们是有用的。
阅读(613) | 评论(0) | 转发(0) |
0

上一篇:灵巧(smart)指针---1

下一篇:引用计数---1

给主人留下些什么吧!~~