房地产代理商的工作是卖房子,而一个为代理商提供支持的软件系统自然要用一个类来描述待出售的房屋:
class HomeForSale { ... };
就如房地产代理商能够很快指出每一间住宅都是独一无二——没有两间是完全一样的。既然如此,为一个 HomeForSale 对象复制出一个副本的想法就显得无意义了。怎么能够复制那些生来就独一无二的东西呢?如果你尝试去复制一个 HomeForSale 对象,那么编译将不通过:
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1); // 尝试复制 h1 :编译失败!
h1 = h2; // 尝试复制 h2 :编译失败!
可惜的是,防止这类代码的编译并不是很直观。通常情况下,如果你不希望一个类支持某种特定种类的功能,只需要不声明相应函数就是了。这一策略对复制构造器和赋值运算符不起作用。因为即使你不声明,但有人尝试调用这些函数,编译器就会自动声明它们。
这将出现一个问题。如果不声明一个复制构造器或者赋值运算符,编译器可能就会生成,你的类就会支持对象复制。而如果声明了这些函数,你的类仍然支持复制。但现在的目标是防止复制!
答案的关键是,所有编译器生成的函数都是public的。为了防止编译器生成这些函数,我们得自行声明它们,但是现在没有什么要求你将这些函数声明为public。因此可以将复制构造器和赋值运算符声明为private。通过显式声明一个函数,就可以防止编译器去自动生成这个函数,而通过将函数声明为 private ,也可以防止人们去调用它。
一般而言这个做法并不绝对安全,这是因为member函数和friend函数仍然可以调用你的private函数。除非你不去定义这些函数,这样如果一些人不慎调用了其中的任一个,会出现一个连接错误(linkage error)。“把成员函数声明为private 的但不去实现它们”,这一窍门已经成为编程常规,因而被用在C++ iostream程序库中阻止“复制行为”。可以参考标准库中 ios_base 、 basic_ios 和 sentry 的实现。你会发现在各种情况下,复制构造器和赋值运算符都声明为private 而且没有定义。
对 HomeForSale 使用这一技巧十分简单:
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&); // 只有声明
HomeForSale& operator=(const HomeForSale&);
};
上面函数中省略了参数名,这是一个惯例。因为这些代码不会实现,并很少用到,那么给定参数名称也就毫无意义了?
通过上面类的声明,编译器会防止客户端程序员尝试复制 HomeForSale 对象,如果不小心在member函数或friend函数中这样做,那么程序将无法连接。
将连接错误移至编译期是可能的(越早发现出错误越好),将复制构造器和赋值运算符声明为 private ,并置于 HomeForSale 的外部,放置在一个专门设计用来防止复制的base class中.。这一基类极其简单:
class Uncopyable {
protected: // 允许派生类存在构造器和析构器
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&); // 但禁止复制
Uncopyable& operator=(const Uncopyable&);
};
为防止 HomeForSale 对象被复制,我们所需要做的仅仅是让其继承 Uncopyable :
class HomeForSale: private Uncopyable {
... // class不再声明复制构造器和赋值运算符。
};
这样做是可行的,因为如果有人(甚至member或friend函数)尝试复制一个 HomeForSale 对象,编译器将会尝试自动生成一个复制构造器和一个赋值运算符。就像 第 12 条中所解释的,这些函数的“编译器生成版”会尝试调用它们基类中的这一部分,显然这些调用不可行,这是因为复制操作在基类中是private。
Uncopyable 的实现和应用颇为微妙,包括不一定得以public继承它(参见第 32 和第 39 条),以及Uncopyable 的析构器不一定得是virtual(参见 第 7 条 )。由于 Uncopyable 不包含任何数据,它有资格作为空基类优化方案(empty base class optimization)(参见第 39 条),但是由于它是一个基类,使用这一技术将导致多重继承(参见第 40 条)。然而,多重继承在某种情况下会使空基类优化失去作用(同样,请参见第 39 条)。总体来说,你可以忽略这些微妙的问题,仅仅使用上面的Uncopyable ,因为它会像所承诺的那样精确地完成工作。你也可以使用它的 Boost 版本(参见 第 55 条)。那个类是noncopyable 。它是一个优秀的类,只是名字不大自然。
需要记住的:
为了禁用编译器自动提供的功能,可将相应的成员函数声明为 private ,同时不予实现。使用Uncopyable这样的base class也是一种做法。