Effective C++ 条款06:若不想使用编译器自动生成的函数,就应该明确拒绝
Effective C++ 条款06:若不想使用编译器自动生成的函数,就应该明确拒绝
在 C++ 中,编译器会默默地为你生成一些函数。但有时候,这些"好心"的自动生成反而会成为代码中的隐患。今天我们来聊聊如何对编译器说"不"。
一、编译器会默默生成哪些函数?
在 C++ 中,如果你声明了一个空类:
classEmpty{};编译器会自动为它生成以下函数(如果它们被需要的话):
| 自动生成的函数 | 说明 |
|---|---|
| 默认构造函数 | Empty() |
| 拷贝构造函数 | Empty(const Empty& rhs) |
| 拷贝赋值运算符 | Empty& operator=(const Empty& rhs) |
| 析构函数 | ~Empty() |
| 移动构造函数 (C++11) | Empty(Empty&& rhs) |
| 移动赋值运算符 (C++11) | Empty& operator=(Empty&& rhs) |
这意味着,下面这段代码完全可以编译通过:
Empty e1;// 调用默认构造函数Emptye2(e1);// 调用拷贝构造函数Empty e3;e3=e1;// 调用拷贝赋值运算符编译器的这种行为在大多数情况下是便利的,但在某些场景下,我们恰恰不希望对象被拷贝或赋值。
二、为什么要拒绝编译器自动生成的函数?
2.1 典型场景
想象一下,你正在实现一个文件句柄管理类:
classFileHandle{public:FileHandle(constchar*filename){fd_=open(filename,O_RDONLY);}~FileHandle(){if(fd_!=-1)close(fd_);}private:intfd_;};如果允许拷贝这个对象,会发生什么?
FileHandlefh1("data.txt");FileHandle fh2=fh1;// 危险!两个对象持有同一个 fd// fh1 和 fh2 析构时都会 close(fd_),导致双重关闭!又或者,你设计了一个单例模式的类,或者一个表示唯一资源标识的类(如数据库连接、锁句柄等),拷贝行为在语义上本身就是不合理的。
2.2 问题的本质
这些类的共同特点是:
- 拷贝会导致资源重复释放或状态不一致
- 拷贝在业务逻辑上没有意义
- 需要保证对象的唯一性
因此,我们必须明确拒绝编译器自动生成拷贝构造函数和拷贝赋值运算符。
三、C++98 时代的解决方案:private + 不实现
在 C++11 之前,Scott Meyers 在《Effective C++》中推荐的做法是:
将不想使用的成员函数声明为
private,并且不予实现。
3.1 具体做法
classFileHandle{public:FileHandle(constchar*filename){fd_=open(filename,O_RDONLY);}~FileHandle(){if(fd_!=-1)close(fd_);}private:// 声明为 private,阻止外部调用FileHandle(constFileHandle&);// 只声明,不实现FileHandle&operator=(constFileHandle&);// 只声明,不实现intfd_;};3.2 为什么这样做有效?
| 层面 | 防护效果 |
|---|---|
| 外部代码 | 无法访问 private 成员,编译报错 |
| 成员函数 / 友元 | 如果误调用,链接时会报错(符号未定义) |
3.3 更优雅的做法:Uncopyable 基类
为了代码复用,可以设计一个专门的基类:
classUncopyable{protected:Uncopyable(){}// 允许派生类构造~Uncopyable(){}// 允许派生类析构private:Uncopyable(constUncopyable&);// 阻止拷贝Uncopyable&operator=(constUncopyable&);// 阻止赋值};使用时只需继承即可:
classFileHandle:privateUncopyable{public:FileHandle(constchar*filename){/* ... */}~FileHandle(){/* ... */}private:intfd_;};这样做的好处是:
- 语义清晰:一眼就能看出该类不可拷贝
- 代码复用:不需要在每个类中重复声明 private 函数
- 错误信息友好:编译错误会指向 Uncopyable,提示"尝试拷贝不可拷贝的对象"
Boost 库中的
boost::noncopyable就是这一思想的实现。
四、现代 C++ 的解决方案:= delete
从 C++11 开始,我们有了更直接、更优雅的语法:
classFileHandle{public:FileHandle(constchar*filename){/* ... */}~FileHandle(){/* ... */}FileHandle(constFileHandle&)=delete;// 明确删除FileHandle&operator=(constFileHandle&)=delete;// 明确删除private:intfd_;};4.1 = delete 的优势
| 特性 | private + 不实现 | = delete |
|---|---|---|
| 语义清晰度 | 间接,需要理解设计意图 | 直接表达"此函数被删除" |
| 错误时机 | 链接期(调用时) | 编译期(任何尝试使用的地方) |
| 代码简洁度 | 需要额外基类或重复声明 | 一行搞定 |
| 现代性 | C++98 兼容 | C++11 及以上 |
4.2 = delete 的额外能力
= delete不仅可以用于自动生成的函数,还可以用于禁止特定类型的隐式转换:
classWidget{public:Widget(intid){/* ... */}Widget(double)=delete;// 禁止 double 隐式转换构造};Widgetw1(42);// OKWidgetw2(3.14);// 编译错误!double 构造函数被删除五、实际应用场景
5.1 资源管理类(RAII)
classMutexLock{public:explicitMutexLock(pthread_mutex_t*mutex):mutex_(mutex){pthread_mutex_lock(mutex_);}~MutexLock(){pthread_mutex_unlock(mutex_);}MutexLock(constMutexLock&)=delete;MutexLock&operator=(constMutexLock&)=delete;private:pthread_mutex_t*mutex_;};5.2 单例模式的基础
classSingleton{public:staticSingleton&getInstance(){staticSingleton instance;returninstance;}Singleton(constSingleton&)=delete;Singleton&operator=(constSingleton&)=delete;private:Singleton()=default;};5.3 不可复制的策略对象
classLoggingStrategy{public:virtualvoidlog(conststd::string&msg)=0;virtual~LoggingStrategy()=default;LoggingStrategy(constLoggingStrategy&)=delete;LoggingStrategy&operator=(constLoggingStrategy&)=delete;};六、常见误区与注意事项
6.1 只声明 private 但不实现,真的安全吗?
在 C++98 中,如果类的成员函数或友元函数内部调用了这些 private 函数,编译器不会报错(因为可以访问 private),但链接器会报错。这意味着错误发现时机被推迟到了链接期。
而= delete能在编译期就发现错误,更加安全。
6.2 需要同时禁用拷贝构造和拷贝赋值吗?
是的!如果只禁用其中一个,另一个仍然可能被误用,导致语义不一致。例如:
classWidget{public:Widget(constWidget&)=delete;// 禁用了拷贝构造// 但拷贝赋值仍然可用!};Widget w1;Widget w2;w2=w1;// 编译通过,但语义上可能有问题6.3 移动语义怎么办?
在 C++11 中,如果你声明了拷贝构造/拷贝赋值为= delete,编译器通常也不会自动生成移动构造/移动赋值。如果需要支持移动语义,需要显式声明:
classFileHandle{public:FileHandle(FileHandle&&other)noexcept;// 移动构造FileHandle&operator=(FileHandle&&other)noexcept;// 移动赋值FileHandle(constFileHandle&)=delete;FileHandle&operator=(constFileHandle&)=delete;};七、总结
| 做法 | 适用场景 | 推荐度 |
|---|---|---|
| private + 不实现 | 需要兼容 C++98 的老项目 | 历史方案 |
| Uncopyable 基类 | C++98 中需要复用不可拷贝语义 | 历史方案 |
= delete | C++11 及以上项目 | 强烈推荐 |
请记住:
- 编译器可以暗自为 class 创建默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。
- 如果不想使用这些自动生成的函数,就应该明确拒绝。
- 在 C++11 及以后,使用
= delete是最清晰、最安全的方式。- 在 C++98 中,将函数声明为
private且不予实现,或使用Uncopyable基类模式。
拒绝编译器的"好意",有时候正是写出健壮代码的关键一步。希望这篇文章对你有所帮助!欢迎在评论区交流讨论。
参考阅读:
- 《Effective C++》第三版,Scott Meyers
- 《C++ Primer》第五版,关于
= delete的章节
