当前位置: 首页 > 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.jsqmd.com/news/984614/

相关文章:

  • 重新定义音乐自由:插件化播放器如何让你真正掌控音乐体验
  • 抗垢水路:SEGE在硬水地区保持清爽
  • idea+git插件+云备份实现项目新分支新建维护
  • 视觉伺服:基于图像的IBVS与基于位置的PBVS
  • 如何让《Honey Select 2》游戏体验全面升级:HS2-HF_Patch终极指南
  • Whisky终极指南:在macOS上轻松运行Windows程序的5个简单步骤
  • 3分钟搞定Windows和Office激活:KMS_VL_ALL_AIO智能脚本全解析
  • 3个月完成全链路升级:300人汽配制造企业SAP升级落地真实案例
  • 告别手忙脚乱:如何用League-Toolkit让英雄联盟游戏体验更丝滑
  • Docker Compose 深度剖析:一文打尽所有配置信息
  • 基于Spring Boot的智能停车导航与管理系统设计与实现
  • MPV播放器终极配置指南:从零构建专业级媒体播放体验
  • 阿里云Linux部署PHP项目:LNMP搭建+域名HTTPS+性能优化全流程
  • 前端周刊2026W22 | React 13周年、TanStack Router、Deno 2.8、Node.js 26、npm 分阶段发布
  • 【人工智能】Gemini回复:“Cherry studio跟Monica 选一个,你选谁?理由是?”
  • 机械泵维修方法?机械泵维修费用多少!
  • Windows系统文件dhcpcsvc6.dll文件丢失找不到问题解决
  • 2026年主流AI招聘工具深度对比:哪款真正能帮你省下80%筛选时间
  • 防割面料采购怎么避坑,选UHMWPE梭织面料供应商为什么更稳
  • NomNom:基于.NET 8的《无人深空》存档数据工程化解决方案
  • Android 权限请求构建器使用指南
  • AlistHelper:告别命令行,用图形界面轻松管理Alist文件服务
  • Cursor Free VIP:终极免费解锁Cursor Pro完整功能的完整指南
  • Smart-SIM工程案例—船舶筏架力学性能快速预测
  • 粗心和专注力有关系吗?
  • Autolabel自动标注工具终极指南:5分钟让AI帮你搞定数据标注难题
  • 艺学启航:为什么企业越来越看重python全栈能力
  • AU-48双麦AI降噪回音消除模组
  • 从“随机抽卡”到“稳定交付”:五步炼成准、稳、实用的提示词
  • 七界梦谭长戟刚鬣怎么打 七界梦谭长戟刚鬣详细打法攻略