HOPL4 笔记 7—11
HOPL 是 History of Programming Languages(编程语言历史)的缩写,是 ACM 旗下的一个会议,约每十五年举办一次。
这是我父的第三篇 HOPL 论文,发表于 2021 年。中文译本 出自 Boolan 之手,不胜感激。
7. 错误处理
使用包含 (值,错误码) 对的类会带来巨大的成本。除了检测错误码的成本外,许多 ABI(应用程序二进制接口)甚至不使用寄存器来传递小的结构体,所以 (值,错误码) 对不仅传递了更多的信息(是通常数量的两倍),而且也使传递的性能有数量级的降低。可悲的是,在许多 ABI 中,尤其那些针对嵌入式系统的 ABI(专为 C 代码设计),这个问题直到今天(2020 年)依然存在。
这些年来,异常处理的性能相对较慢,是因为我们在优化非异常方面花费了大量精力。
具有讽刺意味的是,那些坚定支持异常规约的人转而去帮助设计 Java 了。
一些人相信那些关于异常机制的基于最坏情况和/或不切实际的比较的低效传闻,例如…………,或者使用异常来做简单的错误处理,而不是把异常用于无法在本地处理的错误。
基础库的作者对多种错误处理方案的问题感受最为深刻。他们不知道他们的用户喜欢什么,他们的用户可能有很多不同的偏好。C++17 文件系统库的作者们选择了把接口重复一遍:对于每个操作,他们提供两个函数,一个在错误的情况下抛出异常,另一个函数则通过设置标准库的 error_code 参数将错误码通过参数传递出来
在异常抛出与其处理程序之间的路径上的 noexcept,会把一个异常变成程序终止运行。
异常被添加到 C++ 中的一个重要原因是为了支持那些在发生错误时也决不可以无条件中止的应用。异常仅表示发生了故障,并且从 main() 到抛出点的路径上的任何代码都可以对其进行处理。
我父基本把异常的不适用场景总结出来了:简单的错误处理、严重到可以无条件中止应用的错误发生时。
从根本上讲,我认为 C++ 需要两种错误处理机制:
- 异常——罕见的错误或直接调用者无法处理的错误。
- 错误码——错误码表示可以由直接调用者处理的错误(通常隐藏在易于使用的检测操作中或作为 (值,错误码) 对从函数返回)。
在理想情况下,应该只有两种错误处理的方法,但是我真的不知道如何达到这样一种理想状态。
我要问一个简单而潜在有用的问题:“一个错误要多罕见才被看作是异常情况”?不幸的是,答案是“这要看情况”。这取决于代码、硬件、优化器、异常处理的实现,等等等等。
空间占用问题可能比运行期问题更难解决。
错误码 or 异常,我父也没办法给出准确选择标准的一个问题。只能以简单、罕见、看情况这些宽泛定义的词汇做一个宏观上的总结。所以 Google 编码规范中一刀切地禁止使用异常就可以理解了。
8. C++17:大海迷航
C++17 有很多新的特性,但没有一个我认为称得上重大。
而我却学得很嗨。我父的理由是:“尽管我也喜欢 C++17 中的某些功能,但令人困扰的是这些功能没有统一的主题,没有整体的规划,似乎只是由于可以达到投票多数而被扔进语言和标准库中的一组“聪明的想法””。这就是架构师跟程序员的区别吧?
8.1 构造函数模板参数推导
1 shared_lock lck {m}; // 不需要显式写出锁类型
8.2 结构化绑定
我认为在当前的 C++ 中,
tuple
有点被过度使用了,当多个值并不互相独立的时候,我倾向于使用明确定义的类型我会
using MyType = tuple<int, string, others...>
这样折中一下。
我指出结构化绑定应该引入零开销别名,而任何意味着表示变化的类型转换将导致显著的开销。
是的,对任何可能的(不是全部,比如数组)绑定,都是通过 using 别名实现的,真正的零开销!
最初的提案使用花括号(
{}
)来聚合引入的名字…………然而一些成员,如 Chandler Carruth 和 David Vandevoorde,怕语法上会有歧义,而坚持认为这样会令人困惑,“因为 {} 代表作用域”。所以我们有了[]
语法…………这是个小改动,但我认为是个错误。这个最后一刻的改动,导致了属性表达语法的小小复杂化(比如
[[fallthrough]]
)。这明显站不住脚嘛,统一初始化不就已经拓展了 {} 的用法。
8.3 variant、optional 和 any
不幸的是,这三种类型的设计被分开讨论,好像它们的使用情况毫不相干一样。相对于标准库而言,直接语言支持的可能性似乎从未被认真考虑。
导致的结果就是:
1
2
3
4
5
6
7 optional<int> var1 = 7;
variant<int,string> var2 = 7;
any var3 = 7;
auto x1 = *var1 ; // 对 optional 解引用
auto x2 = get<int>(var2); // 像访问 tuple 一样访问 variant
auto x3 = any_cast<int>(var3); // 转换 any三种截然不同的取值方式……
我认为这三种可辨识 union 的变体只是权宜之计。要解决 union 的问题,函数式编程风格的模式匹配更优雅、通用,潜在也更为高效。…………我们的目的是消除对访问者模式的使用 [Gamma et al. 1994]。
8.4 并发
- scoped_lock——获取任意数量的锁,而不会造成死锁
- shared_mutex 和 shared_lock——实现读写锁
有时,我同很多 C++ 程序员一样在想,“是什么让他们花了这么长时间?”
他们——标委会的老爷们。
8.5 并行 STL
不出意外,委员会中有一些反对的声音,大多数来自于希望为专家级用户提供复杂接口的人。
我十分怀疑这些人都是别的语言阵营派来的奸细。
将来我们会看到专门为并行使用而设计的算法。这正在 C++20 中变为现实。
另一个弱点是,仍然没有取消一个线程的标准方法。例如,在搜索中找到一个对象后,一个线程不能停止其他正在并行执行的搜索。
C++17 的并行算法也支持向量化。这很重要,因为对 SIMD 的优化支持是硬件在单线程性能方面仍然(2017 年后)有巨大增长的少数领域之一。
8.8.3 统一调用语法
回头看,我认为面向对象的写法(如
x.f(y)
)压根就不该被引入。传统的数学式写法f(x,y)
就足够了。坏了……要转向函数范式了?
9. C++20:方向之争
令人震惊的是,C++ 并没有关于动态链接库的标准,也没有标准化的构建系统。
这绝对是阻碍 C++ 生态发展的一个重要阻力。
await 设计无栈、不对称且需要语言支持,而源自 Boost 的设计则使用栈、具有对称控制原语且基于库。无栈协程只能在其自身函数体中挂起,而不能从其调用的函数中挂起。这样,挂起仅涉及保存单个栈帧(“协程状态”),而不是保存整个栈。对于性能而言,这是一个巨大的优势。
我已经等了近 30 年的时间让协程重新回到 C++ 中,我可不想等待一个可能永远不会到来的突破:“最好是好的敌人。”
C++ 程序员必须学会限制编译期计算和元编程的使用,只有在值得为了代码紧凑性和运行期性能而引入它们的地方才使用。
像在 Unix 中一样,管道运算符 | 将其左操作数的输出作为输入传递到其右操作数(例如 A|B 表示 B(A))。
越界访问,有时也称为缓冲区溢出,从 C 的时代以来就一直是一个严重的问题。
1990 年,Dennis Ritchie 向 C 标准委员会提议:“‘胖指针’,它的表示中包括了内存空间以存放运行期可调整的边界。”[Ritchie 1990]。由于各种原因,C 标准委员会没有通过这个提案。在当时,我听到一条极可笑的评论:“Dennis 不是 C 的专家;他从不来参加会议。
哈哈哈……想起了 G 神“I Wrote Python”的段子。
无符号数并不以自然数为模型:无符号数使用模算数,包括减法。比如,如果 ch 是个 unsigned char,ch+100 将永远不会溢出。
我担心的是晦涩难懂的新特性的数量之大会造成危害 [Stroustrup 2018d]。对于非专家来说,它们使得语言变得更加难以学习,代码更加难以理解。
是的……增量学习新标准里的种种语言特性和库特性已经是挑战了,对需要从头学习的新手来说无疑是难上加难。
很多特性具有特殊用途,有些是“专家专用”。不过,有的人总是领会不到,一个对某些人有某种好处的特性,对于 C++ 整体可能是个净负债。
多年来,Bloomberg(那家纽约市的金融信息公司)一直使用一个名为“契约”的实时断言系统去捕获代码中的问题。
新闻界有名的彭博社还是个“技术控”。他们的开源 C++ 库 BDE 被收录在了 cppreference ,我父提到的契约(Contract)就包含其中。
2013 年,一个研究“反射”的研究组(SG7)成立了,…………类似这样的东西很可能会在 C++23 或 C++26 中成为标准。
怪不得我父前文说要彻底消灭预编译宏呢。
10. 2020 年的 C++
所有迹象表明,自 2015 年以来,C++ 的用户数量和使用领域一直在稳步增长。
多亏了 C++11,及时挽回了 C++ 的颓势。这点我父在前文也提到了。
与大多数编程语言社区相比,C++ 社区一向是出奇地无组织和分散。这个问题早已有之,因为我就没有建立组织的才能。
- isocpp.org——C++ 基金会的网站,其中包含与 C++ 有关的新闻,标准化进程相关的信息,以及有用的链接。
- cppreference.com——出色的在线参考资料;它甚至有一个历史部分!
好的软件对我们的文明至关重要。
Qt 依赖于元对象协议(meta-object protocol,缩写为 MOP),因此 Qt 程序还不是标准的 ISO C++ 应用。静态反射使我们最终能够解决这个问题。
C++ 支持多种编程风格(如您坚持,也可以称为“范式”),其背后的想法并不是要让我们选择一种最喜欢的样式进行编程,而是可以将多种风格组合使用,以表达比单一风格更好的解决方案。
当我们需要一个值时,函数是最佳的计算方式,即使——尤其——在编译期。传统模板元编程最好只保留用于计算新的类型和控制结构。
11. 回顾
没有一种语言对所有人和所有事都是完美的。对于这点,没有人比既懂多种语言、又严肃使用其中一种并努力支持它的人了解更多了。
C++ 苦于诞生过早,在现代化的集成开发环境(IDE)、构建系统、图形界面(GUI)系统和 Unicode 问世之前就已经诞生了。
异步的流行、并发/分布式的流行、web 的流行……都应该是推动 C++ 急速进化的主要应用领域,全都错过了。
C++ 社区还缺少一个标准的地方来寻找有用的库。
字符集和图形:C++ 语言和标准库依赖于 ASCII,但大多数应用程序使用某种形式的 Unicode。
字符编码上的混乱真的是劝退级的!
C++ 标准委员会的章程几乎只关注语言和库的设计。这是有局限性的。一直以来,像动态链接、构建系统和静态分析之类的重要主题大多被忽略了。这是个错误。工具是软件开发人员世界的一个重要组成部分,要是能不把它们置于语言设计的外围就好了。
生态!生态!还是生态!