第6条: 要显式禁止编译器为你生成不必要的函数
房地产代理商的工作是出售房屋,而一个为这类代理商提供支持的软件系统自然要用一个类来代表要出售的房屋:
class HomeForSale { ... };
就像每一个房地产代理商能够很快指出的,每一间住宅都是独一无二的——没有两间是完全一样的。既然如此,为一个 HomeForSale 对象复制出一个副本的想法就显得没什么意义了。你怎么能够复制那些生来就独一无二的东西呢?如果你尝试去复制一个 HomeForSale 对象,那么编译器则不应该接受:
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1); // 尝试复制 h1 :不应通过编译!
h1 = h2; // 尝试复制 h2 :不应通过编译!
可惜的是,防止这种复杂问题发生的方法并不是那么的直截了当。通常情况下,如果你希望一个类不支持某种特定种类的功能,你需要做的仅仅是不去声明那个函数。这一策略对复制构造器和赋值运算符就失灵了,这是因为,即使你不做声明工作,而有人尝试调用这些函数,编译器就会为你自动声明它们。
这会使你便陷入困境。如果你不声明一个复制构造器或者赋值运算符,编译器可能就会帮你去做,你的类就会支持对象复制。从另一个角度说,如果你确实声明了这些函数,你的类仍然支持复制。但是现在的目标是防止复制!
解决问题的关键是,所有编译器生成的函数都是公共的。为了防止编译器生成这些函数,你必须自己声明,但是现在没有什么要求你将这些函数声明为公共的。取而代之,你应该将复制构造器和赋值运算符声明为私有的。通过显式声明一个函数,你就可以防止编译器去自动生成这个函数,并且,通过将函数声明为 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 )自然,呃,“非”( non )自然。
需要记住的
l 为了禁用编译器自动提供的功能,你必须将相关的成员函数声明为 private 的,同时不要实现它。使用一个像 Uncopyable 这样的类来完成这一工作。