第18章 virtual
本章是干什么的
本章讲 new、delete、::std::unique_ptr、
virtual 和 RTTI。它们是 C++ 里被滥用得最厉害的四个特性,
也是 C++ 教程最先教的四个特性。我们在本章讲它们,只有一个理由:
你必须读遗留代码。你将来被雇去改的 C++ 代码,几乎全是用它们堆出来的。
新代码里我们不用它们,本章会用实测数据说明为什么。简版结论:
| 遗留特性 | 它真实的代价 | 该写什么 |
|---|---|---|
new / delete |
手工生命周期、看不见的失败路径、失败以遗留的
::std::bad_alloc 异常形式出现 |
带析构函数的拥有型类型;自己拥有存储的容器 |
::std::unique_ptr<T> |
每个对象一次堆往返,而且这个指针在调用点说不清
T 是什么意思 |
值类型;当你真的需要“一个类型对应多种表示”时用类型擦除 |
virtual |
每个对象一个指针、每个类一张表、每个虚函数不管调不调用都要生成、
__cxa_pure_virtual、RTTI、不可移植的 ABI |
模板分派,或者你自己拥有的函数指针表 |
typeid / dynamic_cast |
每个类若干 type-info 对象、被 mangle 过的名字、每次转换一次运行时搜索 | 什么都不用写:用了类型擦除,就没有可转换的目标 |
类级 operator new |
分配策略藏在类型里,成对的 delete 陷阱 | 工厂函数,或者一个由你传进来的区域 (第18.7章) |
| Qt / MFC 风格 GUI | 深继承层次、到处 new、整个程序绑定一个框架 ABI |
Web 前端 + C++ 服务端 (第21章 PWA) |
这个立场来自 $HOME/libraries/Portable-Cpp-Guideline 里的规则:
避免 new、避免 ::std::unique_ptr、
多态优先用类型擦除而不是虚函数、优先用 C 风格的伪 v-table 单例而不是
C++ v-table。
为什么我们反对它们
每个特性一段话,这样本章后面就有东西可证明:
new不是分配的语言原语,而是被焊进你源码里的一个策略决定。 它把你绑死在一个由实现选择的堆上,并且用 1998 年的类型来报告失败 (第18.1章)。 分配失败不可恢复,就该直接终止;把它做成异常是这门语言历史上最大的错误之一 (第18.7章)。::std::unique_ptr不是智能指针,它只是带析构函数的指针。 它只解决泄漏,别的都不解决:不解决类型混淆,不解决 vptr 注入, 不解决所有权含糊,也不解决 ABI。virtual是一张你看不见、控制不了、也裁不掉的派发表。 只要类被用到,编译器就会生成它的每一个虚函数,不管有没有人调用; Itanium ABI 还在上面加了__cxa_pure_virtual和 RTTI (第18.4章)。- 类型擦除用值做同样的事。一个类型、多种表示、没有继承、没有 RTTI、 用户代码里没有分配,而通常出错的那几件事(重复释放、释放后使用、类型搞混、 覆盖 vptr)根本没有出错的空间 (第18.6章)。
本章目录
- 第18.1章
new、delete和::std::unique_ptr—— 它们做什么、代价是什么、 以及为什么vector<unique_ptr<shape>>就是问题的形状 - 第18.2章
virtual究竟是什么 —— vptr、vtable、分派、sizeof, 以及为什么只有配合分配它才“划算” - 第18.3章 每个虚函数都要标注 ——
virtual、override或final,永远要写; 再外加noexcept或throws契约 - 第18.4章 死虚函数膨胀 ——
没人调用的函数也会被编译器生成出来的那 12 KB,以及
__cxa_pure_virtual - 第18.5章 RTTI、
typeid与dynamic_cast—— type-info 对象要多少钱, 以及为什么 RTTI 提供的唯一安全网是一个没人检查的nullptr - 第18.6章 改用类型擦除 —— 用同一个值类型装下多种表示、用指定初始化器写函数指针表, 以及消失掉的那四类 bug
- 第18.7章 类级
operator new与operator delete—— 真必须写的时候怎么写, 以及为什么你不该想写 - 第18.8章 Qt、MFC,以及为什么 GUI 是 C++ 走歪的地方 —— 最被滥用的领域里最被滥用的特性,以及 PWA 这条替代路线
怎么读这一章
18.1 到 18.5 当作识别训练来读:你在学的是在别人的代码里认出这些构造, 并知道每一个换来了什么、花掉了什么。18.6 是你真正要用的那一章。 18.7 只在你必须通过类型来分配内存的那天读。18.8 请在接下“Qt 开发”这份工作之前读。
这里所有规则都是关于你自己的代码的。你仍然得调用用了 virtual
的库;那没问题,fast_io 的设备层也这么做。
你不能做的是在它之上搭新的抽象。