洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目11] 在operator=中要考虑到自赋值问题

当对象为其自身赋值时,就发生了一次“自赋值”:

class Widget { ... };

 

Widget w;

...

w = w;                                   // 自赋值

这样做看上去很愚蠢,但是却是合法的,因此客户一定会这样操做。而且,赋值工作本身并不总是那么容易辨认的。比如:

a[i] = a[j];                       // 潜在的自赋值

如果ij的值相同,那么这就是一次自赋值。另外

*px = *py;                         // 潜在的自赋值

pxpy指向同一处时,上面的代码也是一次自赋值。这些自赋值并不是那么一目了然,它们是由别名造成的:可以通过多种方式引用同一个对象。一般来说,用来操作指向同一类型多个对象的引用或指针的代码都应考虑对象重复的问题。实际上,假如两个对象来自同一层次,即使它们并未声明为同一类型,也要考虑重复问题,这是因为一个基类的引用或指针可以引用或指向其派生类的类型的对象。

class Base { ... };

class Derived: public Base { ... };

void doSomething(const Base& rb,  // rb *pd 可能实际上
                                 Derived* pd);     // 是同一个对象

假设你遵循条目13和条目14中的建议,你将会一直使用对象来管理资源,而且在复制时你将会确保资源管理对象能正确工作。如果上边的假设成立,你的赋值运算符很可能在处理自赋值时将是安全的,你不需要额外关注它。然而如果你试图自己来管理资源(你在编写资源管理类时一定会这样做),此时你很有可能落入这个陷阱中:一个对象尚未用完,但是你却不小心将其释放了。比如说,你创建了一个类,其中放置了一个原始指针来动态分配位图:

class Bitmap { ... };

class Widget {

...

private:

  Bitmap *pb;                      // 指向一个分配在堆上的对象

};

下边给出operator=的一个实现,它在表面看上去很合理,但是如果存在自赋值,它便是不安全的。(它也不是异常安全的,稍后我们讨论这个问题)

Widget&

Widget::operator=(const Widget& rhs)   // operator= 不安全的实现

{

  delete pb;                       // 停止使用当前的位图

  pb = new Bitmap(*rhs.pb);        // 开始使用rhs位图的一份拷贝

  return *this;                    // 参见条目10

}

此处的自赋值问题出现在operator=的内部,*this(赋值操作的目标)和rhs有可能是同一对象。如果它们是,delete便不仅仅销毁了当前对象的位图,同时它也销毁了rhs的位图。Widget的值本不应该在自赋值操作中改变,然而在函数的末尾,它会发现其包含的指针指向了一个已经被删除的对象!

防止这类错误发生的传统方法是:在operator=的最顶端通过一致性检测来监视自赋值:

Widget& Widget::operator=(const Widget& rhs)

{

  if (this == &rhs) return *this;  // 一致性检测: 如果出现自赋值

                                   // 则什么也不做

  delete pb;

  pb = new Bitmap(*rhs.pb);

  return *this;

}

这样可以正常工作,但是上文提到的operator=先前的版本不仅仅是自赋值不安全的,同时也是异常不安全的。尤其在“new Bitmap”语句引发了一个异常(有可能是可分配内存耗尽,或者是Bitmap的拷贝构造函数抛出了一个异常)时,Widget最终将包含一个指向已被删除的Bitmap的指针。这类指针是有毒的。你无法安全的删除它们。你甚至没办法安全的读取它们。此时你所做的唯一一件安全的事情也许就是耗费大量debug的精力去寻找这些指针的出处。

还好,在让operator=在做到异常安全的同时,它同时也能做到自赋值安全。因此,忽略自赋值的问题而把目光集中在异常安全的问题上,就愈加合理了。条目29中深入讨论异常安全的问题,但是通过本条已足以看出:在许多情况下,认真安排语句的次序可以使你的代码做到异常安全(同时也是自赋值安全的)。比方说,这里我们只需要认真考虑:在我们没有把pb所指向的对象复制出来以前,千万不要删除它:

Widget& Widget::operator=(const Widget& rhs)

{

  Bitmap *pOrig = pb;              // 记忆原始的 pb

  pb = new Bitmap(*rhs.pb);        // pb指向*pb的一个副本

  delete pOrig;                    // 删除原始的pb

 

  return *this;

}

现在,如果“new Bitmap”抛出一个异常,pb(及其所在的Widget)就不会被改动。即使没有进行一致性检测,这段代码也可以解决自赋值问题,这是因为我们为原始位图复制出了一个副本,并将原始版本删除,然后让pb指向我们复制出的那个副本。这也许不是解决自赋值问题的最高效的途径,但是这样做确实有效。

如果你考虑到效率问题,你可以重新考虑在程序最开端添加一致性检测。然而在这样做之前先问一下自己:你预计自赋值出现的有多频繁,因为一致性检测也有系统开销。首先它使得代码(源代码和对象)的体积变得稍大一些,其次它也会为控制流引入一个分支,二者都会降低运行的速度。比如说,指令预读、捕获、管线分配等操作的执行效率将会受到影响。

operator=中手动安排语句可以确保实现同时做到异常安全和自赋值安全,这里有一个替代方案:使用一个称为“复制并swap”的技术。由于这一技术更加贴近异常安全的议题,因此我们在条目29中讨论它。这是编写operator=的一个相当普遍的方法,这里看一下它一般实现的方法是值得的:

class Widget {

  ...

  void swap(Widget& rhs);          // 交换*thisrhs中的数据;

  ...                              // 更多细节请参见条目29

};

 

Widget& Widget::operator=(const Widget& rhs)

{

  Widget temp(rhs);                // rhs的数据保存副本

  swap(temp);                      // 使用上边的副本与*this交换

  return *this;

}

通过利用这些事实:(1)一个类的拷贝赋值运算符可以通过传值方式传参;(2)通过传值即可传递某参数的副本(参见条目20),上述主题可以进行以下演化:

Widget& Widget::operator=(Widget rhs)  // rhs是传进对象的一份拷贝

{                                  // 请注意此处是传值方式传递参数

                                  

  swap(rhs);                       // 交换*thisrhs的数据

  return *this;

}

从个人角度来讲,我很担心这一做法会通过牺牲清晰度来换取灵巧性,但是把复制操作从函数体中移出来,放在参数构造的过程中,在一些场合确实能够让编译器生成更加高效的代码。

时刻牢记

在一个对象为自己赋值时,要确保operator=可以正常地运行。可以使用的技术有:比较源对象和目标对象的地址、谨慎安排语句顺序、以及“复制并swap”。

在两个或两个以上的对象完全一样时,要确保对于这些重复对象的操作可以正常运行。

posted on 2007-04-30 18:38 ★ROY★ 阅读(1119) 评论(1)  编辑 收藏 引用 所属分类: Effective C++

评论

# re: 【翻译】Effective C++ (第11条:在operator=中要考虑到自赋值问题)  回复  更多评论   

额的神哦
2008-05-09 14:36 | 网人

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理