Item 12: 拷贝一个 object(对象)的所有 parts(构件)
作者:Scott Meyers
译者:fatalerror99 (iTePub's Nirvana)
发布:http://blog.csdn.net/fatalerror99/
在设计良好的 object-oriented systems(面向对象系统)中,封装了 object(对象)的内部构件,只有两个拷贝 objects(对象)的函数:被恰当地称为 copy constructor(拷贝构造函数)和 copy assignment operator(拷贝赋值运算符)。我们将它们统称为 copying functions(拷贝函数)。Item 5 讲述了如果需要,编译器会生成 copying functions(拷贝函数),而且阐明了编译器生成的版本正像你所期望的:它们拷贝被拷贝 object(对象)的全部数据。
当你声明了你自己的 copying functions(拷贝函数),你就是在告诉编译器你不喜欢缺省实现中的某些东西。编译器对此好像怒发冲冠,而且它们会用一种古怪的方式报复:当你的实现几乎可以确定是错误的时,它们偏偏不告诉你。
考虑一个代表 customers(顾客)的 class(类),这里的 copying functions(拷贝函数)是手写的,以便将对它们的调用记入日志:
void logCall(const std::string& funcName); // make a log entry
class Customer {
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
Customer::Customer(const Customer& rhs)
: name(rhs.name) // copy rhs's data
{
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs)
{
logCall("Customer copy assignment operator");
name = rhs.name; // copy rhs's data
return *this; // see Item 10
}
这里的每一件事看起来都不错,实际上也确实不错——直到 Customer 中加入了其它 data member(数据成员):
class Date { ... }; // for dates in time
class Customer {
public:
... // as before
private:
std::string name;
Date lastTransaction;
};
在这里,已有的 copying functions(拷贝函数)只进行了 partial copy(部分拷贝):它们拷贝了 customers(顾客)的 name,但没有拷贝他的 lastTransaction。然而,大多数编译器即使是在 maximal warning level(最高警告级别)(参见 Item 53),对此也是一声不吭。这是它们在对你自己写 copying functions(拷贝函数)进行报复。你拒绝了它们写的 copying functions(拷贝函数),所以即使你的代码是不完善的,它们也不告诉你。结论显而易见:如果你为一个 class(类)增加了一个 data member(数据成员),你必须确保你也更新了 copying functions(拷贝函数)。(你还必须更新 class(类)中的全部 constructors(构造函数)(参见 Items 4 和 45)以及任何非标准形式的 operator=(Item 10 给出了一个例子)。如果你忘记了,编译器未必会提醒你。)
这个问题最为迷惑人的情形之一是它会通过 inheritance(继承)发生。考虑:
class PriorityCustomer: public Customer { // a derived class
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
PriorityCustomer 的 copying functions(拷贝函数)看上去好像是拷贝了 PriorityCustomer 中的每一样东西,但是再看一下。是的,它拷贝了 PriorityCustomer 声明的 data member(数据成员),但是每个 PriorityCustomer 还包括一份它从 Customer 继承来的 data members(数据成员)的拷贝,而那些 data members(数据成员)根本没有被拷贝!PriorityCustomer 的 copy constructor(拷贝构造函数)没有指定传递给它的 base class constructor(基类构造函数)的参数(也就是说,在它的 member initialization list(成员初始化列表)中没有提及 Customer),所以,PriorityCustomer object(对象)的 Customer 部分被 Customer 的不取得 arguments(实参)的 constructor(构造函数)—— default constructor(缺省构造函数)初始化。(假设它有,如果没有,代码将无法编译。)那个 constructor(构造函数)为 name 和 lastTransaction 进行一次 default initialization(缺省初始化)。
对于 PriorityCustomer 的 copy assignment operator(拷贝赋值运算符),情况只是稍微有些不同。它不会试图用任何方法改变它的 base class data members(基类数据成员),所以它们将保持不变。
无论何时,你打算自己为一个 derived class(派生类)写 copying functions(拷贝函数)时,你必须注意同时拷贝 base class parts(基类构件)。当然,那些构件一般是 private(私有)的(参见 Item 22),所以你不能直接访问它们。derived class(派生类)的 copying functions(拷贝函数)必须调用与它们相对应的 base class functions(基类函数):
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), // invoke base class copy ctor
priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); // assign base class parts
priority = rhs.priority;
return *this;
}
本 Item 标题中的 "copy all parts" 的含义现在应该清楚了。当你写一个 copying functions(拷贝函数),需要保证(1)拷贝所有 local data members(局部数据成员)以及(2)调用所有 base classes(基类)中的适当的 copying function(拷贝函数)。
实际上,两个 copying functions(拷贝函数)经常有相似的函数体,而这一点可能吸引你试图通过用一个函数调用另一个来避免 code duplication(代码重复)。你希望避免 code duplication(代码重复)的愿望值得肯定,但是用一个 copying functions(拷贝函数)调用另一个来实现它是错误的。
用 copy assignment operator(拷贝赋值运算符)调用 copy constructor(拷贝构造函数)是没有意义的,因为你这样做就是试图去构造一个已经存在的 object(对象)。这太荒谬了,甚至没有一种语法来支持它。有一种语法看起来好像能让你这样做,但实际上你做不到,还有一种语法采用迂回的方法这样做,但它们在某种条件下会对破坏你的 object(对象)。所以我不打算给你看任何那样的语法。无条件地接受这个观点:不要用 copy assignment operator(拷贝赋值运算符)调用 copy constructor(拷贝构造函数)。
尝试一下另一种相反的方法——用 copy constructor(拷贝构造函数)调用 copy assignment operator(拷贝赋值运算符)——这同样是荒谬的。一个 constructor(构造函数)初始化新的 objects(对象),而一个 assignment operator(赋值运算符)只能应用于已经初始化过的 objects(对象)。借助构造过程给一个 object(对象)assignment(赋值)将意味着对一个 not-yet-initialized object(尚未初始化的对象)做一些事,而这些事只有用于 initialized object(已初始化对象)才有意义。简直是胡搞!禁止尝试。
作为一种代替,如果你发现你的 copy constructor(拷贝构造函数)和 copy assignment operator(拷贝赋值运算符)有相似的代码,通过创建一个供两者调用的第三方 member function(成员函数)来消除重复。这样一个函数一般是 private(私有)的,而且经常叫做 init。这一策略是在 copy constructor(拷贝构造函数)和 copy assignment operator(拷贝赋值运算符)中消除 code duplication(代码重复)的安全的,被证实过的方法。
Things to Remember
- copying functions(拷贝函数)应该保证拷贝一个 object(对象)的所有 data members(数据成员)以及所有的 base class parts(基类构件)。
- 不要试图依据一个 copying functions(拷贝函数)实现另一个。作为代替,将通用功能放入一个供双方调用的第三方函数。