当前位置: 首页 > news >正文

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 中需要复用不可拷贝语义历史方案
= deleteC++11 及以上项目强烈推荐

请记住

  • 编译器可以暗自为 class 创建默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。
  • 如果不想使用这些自动生成的函数,就应该明确拒绝。
  • 在 C++11 及以后,使用= delete是最清晰、最安全的方式。
  • 在 C++98 中,将函数声明为private且不予实现,或使用Uncopyable基类模式。

拒绝编译器的"好意",有时候正是写出健壮代码的关键一步。希望这篇文章对你有所帮助!欢迎在评论区交流讨论。


参考阅读

  • 《Effective C++》第三版,Scott Meyers
  • 《C++ Primer》第五版,关于= delete的章节
http://www.zskr.cn/news/1496361.html

相关文章:

  • 重新定义音乐自由:插件化播放器如何让你真正掌控音乐体验
  • 3分钟搞定Windows和Office激活:KMS_VL_ALL_AIO智能脚本全解析
  • Whisky终极指南:在macOS上轻松运行Windows程序的5个简单步骤
  • Docker Compose 深度剖析:一文打尽所有配置信息
  • 告别手忙脚乱:如何用League-Toolkit让英雄联盟游戏体验更丝滑
  • 基于Spring Boot的智能停车导航与管理系统设计与实现
  • MPV播放器终极配置指南:从零构建专业级媒体播放体验
  • 2026年主流AI招聘工具深度对比:哪款真正能帮你省下80%筛选时间
  • AlistHelper:告别命令行,用图形界面轻松管理Alist文件服务
  • Autolabel自动标注工具终极指南:5分钟让AI帮你搞定数据标注难题
  • Smart-SIM工程案例—船舶筏架力学性能快速预测
  • 芯片制造:Bandgap(带隙基准源)电路中重要的模块和功能
  • 自动装盘机倒瓶检测系统的传感器选型与信号处理
  • PCDN服务SLA设计:承诺多少节点在线才合理?
  • Playwright MCP Docker 部署:mcr 镜像、浏览器工具和权限配置
  • 鸿蒙App开发--心愿池的动画特效:投币动画与进度条
  • 拒绝“胶水架构”:大模型时代,如何用统一任务基座破解 AI 研发的技术债?
  • 打造你的专属音乐库:LXMusic音源配置实战指南
  • 亚马逊270天库存生死线!超龄库存清仓全攻略
  • 终极指南:5分钟在Mac上实现Android手机USB网络共享
  • 计算机毕业设计之智能家居安全体系设计及实现
  • 手动创建Gazebo环境与自适应控制PX4
  • Windows 10 Android子系统移植实战指南:架构解析与深度部署方案
  • AI驱动的自动化——金融、物流与医疗行业的转型
  • 前端八股文面经大全:美团前端暑期实习一面(2026-06-08)·面经深度解析
  • equals 和 hashCode 方法:使用它们的原因和位置,以及它们的工作原理
  • 小程序开发入门:从零掌握基础代码结构
  • 华硕笔记本终极性能调校指南:5分钟掌握G-Helper完整配置
  • 逆水寒手游装备系统主要ui界面拆解
  • 【软件发布】光明正大的看小说而不被发现的摸鱼软件:伪输入法(FakeType)