C++ 设计缺陷
Created @ 2014-08-01 21:46 rev2 2014-08-01 22:14, rev3 2015-09-15, markdown @ 2015-09-15.
0 不识时务的妥协
特别地,兼容(而不是取代) C 。
1 类型系统
设计上烂得最广泛的毫无疑问是类型系统——特别地,缺少很多本来该在类型系统内的东西。
1.1 基本类型
基本类型在(所谓的 fundamental types ) C 的基础(所谓的 basic types )上重写了一遍,有微妙的不同(如宽字符类型),但换汤不换药,缺点差不多一个没少。
1.2 标准转换
继承了 C 的低质量的转换:
- integer promotion
- array-to-pointer conversion
- function-to-pointer conversion
1.3 非一等类型(first class types)
继承了 C 对数组和函数类型的差别待遇。除了上面的转换,作为在函数参数和返回值也有反直觉的限制。
1.4 动态类型(dynamic types)
添加了动态类型却支持有限,实际上搞出了 ABI 的坑——缺乏公开规范,乃至同一个体系结构和操作系统的二进制代码都没法兼容。
题外话,为了描述规则的清晰性 ISO C99 添加了有效类型(effective type) ,实际上相当于动态类型,不过没有明确的运行时支持。这反倒不影响实现。
1.5 对象类型
1.5.1 语义
和 C 一样, C++ 的对象(object) 指存储。和 C 不同, C++ 对象的销毁同时可能通过非平凡析构函数(non-trivial destructor) 的调用而具有副作用(side effect) 。
但事实上,要求存储和要求生存期边界引发副作用是正交的特性。 C++ 显然没能处理好这一点:为什么一个只关心析构副作用的对象仍然必须占据存储?
此外,类类型是对象类型。不使用特殊规则,基类或数据成员子对象将会占据存储——即便实际上没用到。因此语言不得不使用特别规则进行空基类优化(empty base optimization) ——而且对空数据成员的位置还有限制。考虑到许多时候这里的类只是和静态类型系统交互,这种冗余的语义本应是毫无意义的。
1.5.2 分类学
C 的对象类型包括 void
,而 C++ 把 void
独立出来。但是,仍然有一些容易混淆之处: void*
是 object pointer type ,但不是 pointer-to-object type 。
1.5.3 布局
添加了和 C 不一样的布局,却不提供足够的、明确的、可移植的支持,导致基础设施的失效:
- 如
offsetof
ISO C++11 提供了标准布局类(standard-layout class) 的概念,基本继承于之前的 POD(plain-old-data) class 。然而这个概念的定义在之后版本的标准中仍有微妙的变化,并不容易被理解。
标准布局类型(standard-layout type) 的概念构筑在标准布局类之上。其它对象布局就没有附加约束了,如果需要可移植的 ABI ,几乎只能使用标准布局类型。这排除了许多特性,例如具有虚函数而不满足标准布局要求的多态类(polymorphic class) 。
1.6 值类别(value category) 和引用
添加了引用类型,想取代 lvalueness (参见 WG21 最早公开的 paper ),后来却坑了。
在表达式求值的特殊规则下,引用类型的表达式被视为被引用的实体。这种特殊规则减少了那么一点其它一些语言规则(如 ADL )的描述,却实际上使引用类型也不完全是一等实体了。
- 现在还越搞越复杂,搞出了多种引用类型和 value category ,依然不视为类型
- 并且语义和用例容易被误解,且很多时候难以避免冗余编码
1.7 添加了参数化多态
却对高阶参数化类型(模板模板参数)有莫名其妙的限制导致不可用。
1.8 添加了实质上的参数化类型
却独立于名义类型系统(nominal type system) 之外。
概念(concept) 有望部分地改善这点,但无法改变基础设计分裂的现状。
1.9 添加了类型推导(type deduction)
却不足够支持一般意义的类型推断(type inference) ——例如在构造函数上不起作用,需要额外的工厂方法(factory method) 作为惯用法。
1.10 添加了实质上的重写系统
却没有完善的类型系统支持,如:
- 基于类型的模式匹配
- 自定义重载规则
2 一些深刻的设计失误
是差不多所有Algol60直系后裔都存在的功能缺失。
2.1 同像性(isomorphism)
这导致语言中不得不对反射做出特别对待。偏偏到现在还没有。
2.2 底层高级流程抽象
具体点说,能代替 J operator 或者 call/cc
之类的东西。
这同时导致诸如 coroutine 等依赖基本控制流的抽象不能在不添加语言特性的前提下被可移植地高效实现;同时也有下面的异常问题。
2.3 活动记录抽象
这导致典型实现及其用户过于依赖特定于体系结构的运行时栈。
实际上还同时有栈溢出 UB 这种无解的设定。
3 一些本不适合内建的特性相关问题
内建特性在兼容性和扩展性上尤其容易出现问题,特别是存在不够显然的实现的时候。
3.1 异常不得不被实现为内建的,某种意义上是缺乏上述两者的直接结果——这导致了另外的问题:
- 例如ABI兼容性——同一个体系结构和操作系统的同一版本的同一个实现,使用不同不同异常模型时仍然可能不兼容
- 再如 WG21/N4049
3.2 面向对象支持——关键是面向对象本身就没有个相对统一的认识。
- 内建导致之后扩充多分派等需要顾及的太多而最终放弃,远不如 CLOS 之类的库解决方案灵活
- 同样在一些方面导致和加重了ABI问题
4 阶段(phase) 相关
某种意义上是缺乏同像性的副作用。
ISO C++ 规定的实现阶段数量过多,且过于琐碎。
这导致细节难以处理,也难以提高实现质量。
4.1 预处理被标准化,但是细节比较混乱,导致了很多演进上的问题。
- 像
#ifdef
和#if defined
的冗余
4.2 缺少可用的模块系统
ISO C++17 似乎不能保证引入……
4.3 存在语言链接支持却缺乏可移植的语言嵌入。
只有一个 asm
还不指望能用。
5 至于文法/语法嘛……
本来懒得说,不过因为逗到一定境界也顺便提一下好了。
5.1 兼容 C 的渣声明符中缀语法
特别地, C++11 引入 trailing-return-type
后也没法放弃旧的语法,并存的结果是增加复杂性。
5.2 比 C 更多的文法歧义
最著名的坑应该就是函数声明优先于初始化对象了。另外还有几个类似的地方。
5.3 标点问题
因为基本源字符集的限制,使用大于(<
) 和小于(>
) 的尖括号(angle brackets) 代替了数学上习惯的看起来更扁一点的括号(chevrons) “〈”和“〉”。这导致了极大的解析上的复杂性,同时还需要增设特例规则(记号 >>
和记号序列 > >
歧义)。
5.4 少数 C 的语法的不规则延伸
如 new
表达式。
除了单独的消歧义规则的复杂性外,特异的语法规则导致用宏替换实现一些操作更困难,也导致语言规范的复杂(有多少人能分清 type-id 和 new-type-id 呢)。