真实的房地产代理商的工作是出售房屋,而一个为房产代理商提供支持的软件系统自然要用一个类来代表要出售的房屋:
class HomeForSale { ... };
所有房地产代理商能够很轻松的指出,每套房产都是独一无二的——没有两套是完全一样的。既然如此,为一个HomeForSale对象复制出一个副本的想法就显得没什么意义了。你怎么能够复制那些生来就独一无二的东西呢?如果你尝试去复制一个HomeForSale对象,那么编译器则不应该接受:
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1); // 尝试复制h1:不应通过编译!
h1 = h2; // 尝试复制h2:不应通过编译!
可惜的是,防止这种复杂问题发生的方法并不是那么直截了当。通常情况下,如果你希望一个类不支持某种特定的功能,你需要做的仅仅是不去声明那个函数。然而这一策略对复制构造函数和拷贝赋值运算符就失效了,这是因为,即使你不做声明,而一旦有人尝试调用这些函数,编译器就会为你自动声明它们(参见条目5)。
这会使你便陷入困境。如果你不声明一个复制构造函数或者赋值运算符,编译器可能就会帮你去做,你的类就会支持对象复制。从另一个角度说,如果你确实声明了这些函数,你的类仍然支持复制。但是现在的目标是防止复制!
解决问题的关键是,所有编译器生成的函数都是公共的。为了防止编译器生成这些函数,你必须自己声明,但是现在没有什么要求你将这些函数声明为公共的。取而代之,你应该将复制构造函数和赋值运算符声明为私有的。通过显式声明一个函数,你就可以防止编译器去自动生成这个函数,同时,通过将函数声明为private的,你便可以防止人们去调用它。
差不多了。但是这一方案也没有那么傻瓜化,这是因为成员函数和友元函数仍然可以调用你的私有函数。除非你足够的聪明,没有去定义这些成员或友元。如果一些人由于疏忽大意而调用了其中的任意一个,他们会在程序连接时遇到一个错误。把成员函数声明为private的但是不去实现它们,这一窍门已经成为编程常规,在C++的I/O流的库中的一些类,都会采用这种方法来防止复制。比如,你可以参考标准库中ios_base、basic_ios和sentry的实现。你会发现在各种情况下,复制构造函数和拷贝赋值运算符都声明为private而且没有得到定义。
对HomeForSale使用这一技巧十分简单:
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&); // 只有声明
HomeForSale& operator=(const HomeForSale&);
};
你会发现我省略了函数参数的名称。这样做并不是必需的,这仅仅是一个普通惯例。毕竟这些代码不会得到实现,而且很少会用到,那么给这些参数命名又有什么用呢?
通过上文中类的声明,编译器会防止客户尝试复制HomeForSale对象,如果你不小心在成员函数或者友元函数中这样做了,那么你的程序将无法得到连接。
如果你将复制构造函数和拷贝赋值运算符声明为private的,并且将二者移出HomeForSale的内部,放置在一个专门设计用来防止复制的基类中,那么在编译时就排除这些原本是连接时的错误便成为可能(这是件好事——早期发现错误要比晚些更理想)。这一基类极其简单:
class Uncopyable {
protected: // 允许派生类存在构造函数和析构函数
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&); // 但禁止复制
Uncopyable& operator=(const Uncopyable&);
};
为了防止HomeForSale对象被复制,我们所需要做的仅仅是让其继承Uncopyable:
class HomeForSale: private Uncopyable {
... // 这一类不再声明复制构造函数和赋值运算符
};
这样做是可行的,这是因为如果有人(甚至是一个成员或友元函数)尝试复制一个HomeForSale对象,编译器将会尝试自动生成一个复制构造函数和一个拷贝赋值运算符。就像条目12中所解释的,这些函数由编译器自动生成的版本会尝试调用它们基类中的这一部分,显然这些调用只能吃到闭门羹,这是因为复制操作在基类中是私有的。
Uncopyable的实现和应用存在一些微妙的问题,诸如继承自Uncopyable的类不一定必须为public的(参见条目32和条目39),Uncopyable的析构函数不一定必须为虚函数(参见条目7)。由于Uncopyable不包含任何数据,它适合作为空基类优化方案(参见条目39),但是由于它是一个基类,使用这一技术将导致多重继承(参见条目40)。然而,多重继承在某种情况下会使空基类优化失去作用(同样,请参见条目39)。总体来说,你可以忽略这些微妙的问题,仅仅使用上文中的Uncopyable,因为它会像所承诺的那样精确地完成工作。你也可以使用Boost版本(条目55)。那个类叫做noncopyable。它是一个优秀的类,我只是发现它的名字显得有些不自然(un-natural),呃,“非”自然(non-natural)。
时刻牢记
l 为了禁用编译器自动提供的功能,你必须将相关的成员函数声明为private的,同时不要实现它。方法之一是:使用一个类似于Uncopyable的基类。