当对象对其自身赋值时,就发生了一次“自赋值”:class Widget { ... }; Widget w; ... w = w; // 自赋值
这样做似乎没什么意义,但是合法,因此以后我们假设客户可能会这样做。而且,赋值工作本身并不总是那么容易辨认的。比如:
a = 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 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=具备“异常安全性”往往自动得到“自赋值安全”,可以把焦点集中在“异常安全性”(exception safety)上,而忽略自赋值的问题。第 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)以by value方式传递东西会有一份副本(参见第 20 条):Widget& Widget::operator=(Widget rhs) // rhs 为被传对象的一份副本 { // passed in —note pass by val 传值 swap(rhs); // swap *this's data with the copy's return *this; }
上述灵活的修补牺牲了清晰性,但是把“复制操作”从函数体函数本体内移至“函数参数构造阶段”,在一些场合确实能够编写出更加高效的代码。
需要记住的
1、在一个对象为自己赋值时,要确保 operator= 可以正常地运行。可以使用的技术有:比较源对象和目标对象的地址、谨慎安排语句、以及“复制并交换”。
2、在两个或两个以上的对象完全一样时,要确保对于这些重复对象的操作可以正常运行。
第11条: 在 operator= 中处理“自我赋值”
最新推荐文章于 2023-11-11 12:11:35 发布