公告:本站提供编程开发方面的技术交流与分享,打造最佳教程网,希望能为您排忧解难!

Effective C++ 第11条:在operator=中要考虑到自赋值问题

Effective C++ 第11条:在operator=中要考虑到自赋值问题

更新时间:2013-03-16 13:13:11 |

第11条: 在 operator= 中要考虑到 自赋值问题

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

class Widget { ... };

 Widget w;
...

w = w;    // 自赋值

这样做看上去没什么意义,但这是合法的,因此以后我们假设客户端程序员可能会这样做。而且,赋值工作本身并不总是那么容易辨认的。比如:

a[i] = a[j];   // 可能发生自赋值

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

*px = *py;    // 可能发生自赋值

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

class Base { ... };

 class Derived: public Base { ... };

// rb 和 *pd 可能实际上是同一个对象 
void doSomething(const Base& rb,   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 。这类指针是有毒的。你无法安全的删除它们。你甚至没办法安全的读取它们。此时你所做的唯一一件安全的事情也许就是耗费大量的精力去排查 bug 。

还好,在让 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 )没有被改动。即使没有进行一致性检测,这段代码也可以解决自赋值问题,这是因为我们复制出了原始位图的一个副本,并且删除了原始副本,然后指向我们复制出的那个副本。这也许不是解决自赋值问题的最高效的途径,但是这样做确实有效。

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

为了使 operator= 的实现对异常和自赋值都保证安全,必须为其手动安排语句,这里还有另一个途径:使用一个称为“复制并交换”的技术。这一技术更加贴近异常安全问题,所以我们在第 29 条中讨论它。但是它在编写 operator= 时得到了十分普遍的应用,看一下它实现的方法是十分值得的:

class Widget {

  ...

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

  ...      // 更多细节请参见第 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 is a copy of the object

{                   // passed in — note pass by val

  swap(rhs);       // swap *this's data with the copy's

  return *this;

}

从我个人角度来讲,我很担心这一点,这个手段会将清晰度作为“祭祀品”摆放在灵巧性的“祭坛”上,但是把复制操作从函数体中移出来,放在参数结构中,在一些场合确实能够编写出更加高效的代码。

需要记住的

  • 在一个对象为自己赋值时,要确保 operator= 可以正常地运行。可以使用的技术有:比较源对象和目标对象的地址、谨慎安排语句、以及“复制并交换”。 
  •  在两个或两个以上的对象完全一样时,要确保对于这些重复对象的操作可以正常运行。
最佳教程网

最大的技术交流平台 www.goodxyx.com© CopyRight 2011-2013, All Rights Reserved

浙ICP备11033019号