条款13 复制操作
复制构造(copy construction)和复制赋值(copy assignment)是两种不同的操作。从技术角度来说,它们之间没有丝毫的关联,但从“社会”角度来说,它们一般被放到一起,同时出现,并且必须兼容:
- class Impl;
- class Handle {
- public:
- //...
- Handle( const Handle & ); // 复制构造函数
- Handle &operator =( const Handle & ); // 复制赋值操作符
- void swap( Handle & );
- //...
- private:
- Impl *impl_; // 指向Handle的实现
- };
复制操作的影响是如此深远,以致于遵守惯例变得异常重要。这两个操作总是被成对地声明,具有如上所示的签名(另请参考“auto_ptr非同寻常”[条款43]以及“禁止复制”条款 [32])。也就是说,对于一个类X而言,复制构造函数应该被声明为X(const X &),而复制赋值操作符则应该被声明为X &operator =(const X &)。通常来说,和传统的非成员形式的swap相比,如果成员形式的swap实现具有性能或异常安全的优势,那么定义一个成员函数swap往往是个好主意。典型的非成员形式的swap实现是很直观的:
- template <typename T>
- void swap( T &a, T &b ) {
- T temp(a); // 调用T的复制构造函数
- a = b; // 调用T的复制赋值操作符
- b = temp; // 调用T的复制赋值操作符
- }
这个swap(与标准库中的swap一致)根据类型T的复制操作进行定义,如果T的实现短小简单,这种方式就会工作得很好;但如果T是一个庞大而复杂的类,这种方式就会花不小的开销。对于Handle这样的类,我们有更好的方式,那就是交换指向各自实现的指针。
- inline void Handle::swap( Handle &that )
- { std::swap( impl_, that.impl_ ); }
还记得一个旧的喜剧片中讲到的“如何拿到一百万美元而又不用交税”这个情节吗?首先,拿到一百万美元……①下面的情景与它类似,展示了如何编写一个异常安全的复制赋值操作。首先,要得到一个异常安全的复制构造函数和一个异常安全的swap操作。剩下的事情就好办了:
- Handle &Handle::operator =( const Handle &that ) {
- Handle temp( that ); // 异常安全的复制构造
- swap( temp ); // 异常安全的swap
- return *this; // 我们假定temp的析构不会抛出异常
- }
对于句柄类(handle class)来说,这项技术工作得尤其好。句柄类是这样的一种类,它主要或全部是由一个指向其实现的指针构成。如前例所示,为句柄类编写异常安全的swap乃是小菜一碟,且效率极高。
对于复制赋值的这个实现来说,微妙之处在于复制构造的行为必须和复制赋值的行为“兼容”。尽管它们是不同的操作,然而此处存在一个影响深远的惯用假定,就是它们产生的结果不应该有区别。也就是说,不管是写成
- Handle a = ...
- Handle b;
- b = a; // 将a赋给b
还是写成
- Handle a = ...
- Handle b( a ); // 用a来初始化b
b的结果值和将来的行为都应该没有差别,不管它是通过赋值还是通过初始化而得到那个值的。
当使用标准容器时,这种兼容性尤其重要,因为它们的实现常常用复制构造来代替复制赋值,当然也就期望两种操作产生一致的结果(参见“placement new”[条款35])。
一个或许更常见的复制赋值实现具有如下的结构:
- Handle &Handle::operator =( const Handle &that ) {
- if( this != &that ) {
- // 进行赋值……
- }
- return *this;
- }
这种对自身赋值所执行的检查往往是出于正确性的考虑(有时也是出于效率方面的考虑)。更确切地说,为了确保赋值表达式的左操作数(this)和右操作数(例如that)具有不同的地址。
大多数C++(www.cppentry.com)程序员在职业生涯里,都摆弄过实现虚拟复制赋值(virtual copy assignment)的想法。这种做法合法但过于复杂,所以别那么做。应该采用clone(克隆)取而代之(参见“虚构造函数与Prototype模式” [条款29])。