C++ 技巧:明确拒绝编译器自动生成的拷贝函数
场景描述:考虑一个用于表示'待售房屋(HomeForSale)'的类。
问题引入:在现实世界中,每一处房产都是独一无二的(房屋的朝向、位置、户型等等),不可能存在两套完全相同的房屋。因此,从语义上讲,为一个 HomeForSale 对象创建副本并不合理。基于上述建模假设,我们应当明确地禁止 HomeForSale 对象的拷贝行为,使得任何试图对其进行拷贝的操作都以失败告终。
实现 1(不可行):不声明相应的成员函数
设计思路:通常情况下,如果你不希望某个 class 支持某种特定机能,只要不声明相应的成员函数即可。然而,这一策略对拷贝构造函数和拷贝赋值运算符却并不适用。因为,如果你没有显式声明它们,而又有人尝试使用拷贝操作,编译器会自动为你生成这些函数(见'条款 5'),从而使得拷贝操作得以成功执行。
实现 2(可行,不完美):private 未定义函数
设计思路:声明相应的成员函数为 private,并且(故意)不予实现(定义)。这一技巧早已被广泛接受,并被标准库用于显式禁止对象的拷贝行为。典型例子便是 C++ 的 iostream 程序库:在标准库实现代码中查看 ios_base、basic_ios 以及 sentry 等类即可发现,它们的拷贝构造函数和拷贝赋值运算符均被声明为 private,且未提供任何定义,从而有效地阻止了拷贝操作。
- 予以声明:通过明确声明成员函数,从而阻止编译器自动(暗自)创建其专属版本(所有编译器产生的函数都是 public)。
- private:通过令成员函数为 private,阻止 non-member 函数(即外界)调用(编译错误),从而禁止拷贝操作。
- 不予实现:通过只提供声明而不提供定义,阻止 member 函数和 friend 函数(即内部)调用(链接错误),从而禁止拷贝操作。
实现细节:上述代码未指定函数参数的名称。事实上,参数名称并非必需,它们的存在更多只是出于可读性和习惯考虑。既然这些函数注定不会被实现,也几乎不可能被调用,因此为参数命名自然也就失去了意义。
运行结果:当 non-member 函数企图执行拷贝操作,编译器将会报错(无法调用 private 接口);当 member 函数或 friend 函数企图执行拷贝操作,链接器将会报错(无法找到接口对应的实现/定义)。
实现 3(可行,C++03 推荐):private 继承自 Uncopyable 基类
设计思路:将错误从链接期提前到编译期(这是一件好事,越早侦测出错误越好)。另外,所有需要'禁止拷贝属性'的类,唯一需要做的就是继承自 Uncopyable 基类。
实现细节(基类):
- Uncopyable 访问权限:将构造函数和析构函数设置为 protected,禁止外界调用(即禁止基类对象的构造和析构);但允许派生类调用(即允许派生类对象的构造和析构)。
- 拷贝控制:将拷贝构造和拷贝赋值设置为 private,禁止派生类调用(即禁止派生类对象的拷贝)。
class Uncopyable { protected: Uncopyable() {} ~Uncopyable() {} private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&); };


