HOPL4 笔记 1-5

HOPL 是 History of Programming Languages(编程语言历史)的缩写,是 ACM 旗下的一个会议,约每十五年举办一次。
这是我父的第三篇 HOPL 论文,发表于 2021 年。中文译本 出自 Boolan 之手,不胜感激。

1. 前言

有一种流传广泛的谬见,就是程序员希望他们的语言是简单的。 当你不得不学习一门新的语言、不得不设计一门编程课程、或是在学术论文中描述一门语言时,追求简单显然是实情。 对于这样的用途,让语言干净地体现一些明确的原则是一个明显的优势,也是理想情况。 当开发人员的焦点从学习转移到交付和维护重要的应用程序时,他们的需求从简单转移到全面的支持、稳定性(兼容性)和熟悉度。人们总是混淆熟悉度和简单,如果可以选择的话,他们更倾向于熟悉度而不是简单。

这也是我在前有 C 后有 Rust 的情况下继续跟踪并学习 C++ 新标准的原因:已经上了贼船了,没办法。

任务库 [Stroustrup 1985a,c] 是一个基于协程的库,支持事件驱动的仿真(例如随机数生成),与替代方案相比是非常高效的,甚至可以运行在很小的计算机上。例如,我在 256KB 的内存中运行了 700 个任务的仿真。任务库在 C++ 早期非常重要,是贝尔实验室和其他地方许多重要应用的基础。然而,它有点丑陋,并且不容易移植到 Sun 的 SPARC 体系结构,因此大多数 1989 年以后的实现都不支持它。2020年,协程才刚刚回归。

我父在 1979 年设计 C++ 时实现了两个库:task(协程)和用宏参数化的 vector。虽然在标准化过程中删的删改的改,也足见其个人高雅的技术品味。41 年后,笨重老朽的标委会终于准入了协程……

2. 背景:C++ 的 1979–2006

我的办公室就在 Dennis Ritchie 和 Brian Kernighan 走廊对面。

贝尔实验室就是万神殿!后来桃李半导体天下的仙童也是出自贝尔,“软硬兼施”。一百年过去了,我们的贝尔在哪里?

最初我实现的是一个预处理器,它将“带类的 C” 差不多逐行翻译成 C。1982 年,在“带类的 C”的用户数量增长到了几十人的时候,这种方法已经显得无法把控了。所以我写了一个传统的编译器,叫作 Cfront,1983 年 10 月第一次给别人使用。

原来我父也挺会“对付”的,用语法糖验证了 C++ 的技术可行性和市场接受度后才开始考虑专属编译器的问题。嘿嘿,学到了。
1979.4 月开始,4 年半后才实现了第一个编译器。基础设施的建设实属不易啊。向流浪地球致敬,在现在这种浮躁的环境下迈出了扎实的一步。游戏科学加油啊……

直到今天,我仍然认为构造函数和析构函数是 C++ 的真正核心。

不要陷入对完美的徒劳追求。

不管是业务开发、架构设计,同样适用。

应取消预处理器的使用。

我父有点飘了……虽然后来的模版取代了一部分宏的能力,但是因为一直没有静态反射机制,# 和 ## 依然是逆天的存在。

不要给 C++ 以下的低级语言留有余地(汇编语言除外)。

嘿嘿嘿,我父前边才说完“过于狭隘的 C 和 C++ 爱好者之间偶尔会恶语相向”,你这条设计规则才是祸根吧……

1984 年:运算符和函数重载——除了算法和逻辑运算符外,还包括:允许用户定义 =(赋值)、()(调用;支持函数对象)、[](下标访问)和 ->(智能指针)。

前些天面了一小伙,居然函数对象的概念都没听过。我连换仿函数、functor 几个叫法,生怕是因为翻译问题。父啊,你的伟作现在就这样被人糟蹋呢……

我从未使用过“C++ 是一种面向对象的编程语言”这种说法,这件事很多人并不知道,或者因为感到有些尴尬而有意忽略了。那时候,我的标准描述是 C++ 是一门偏向系统编程的通用编程语言,它是

  • 更好的 C
  • 支持数据抽象
  • 支持面向对象编程
  • 支持泛型编程

这个说法过去和现在都是准确的,但不如“万物皆对象”这样的口号令人兴奋!

“视 C++ 为一个语言联邦”。OOP 或 TMP,没有高低之分。

经过了惯例性的、大约十年的工作,该委员会终于发布了第一个标准:C++98。我和许多其他人自然更愿意更快地输出一个标准,但是委员会规则、过度的雄心和各种各样的延迟使我们在时间表方面与 Fortran、C 和其他正式标准化的语言站在了同一起跑线上。

1989.12 月 ANSI C++ 标委会就已经成立,十年的时间啊,你们在搞什么?

dynamic_cast 是一个运行期操作,依赖于存储在 Shape 的虚拟函数表中的数据。它通用、易用,并且与其他语言类似的功能一样高效。然而,dynamic_cast 变得非常不受欢迎,因为它的实现往往是复杂的,特殊情况下手动编码可能更高效(可以说这导致 dynamic_cast 违反了零开销原则)。

是的。有层次的、设计良好的架构中,两个具体的实现子类往往是精确的一对一关系,这种良好的设计保证了在最上层的实现类中执行静态类型转换总是安全的,不需要运行时检查。

3. C++ 标准委员会

SG4 网络,目前处于休眠状态,因为其结果正在等待被合并到标准中。

三年又三年……到现在都还没有 executor 被批准进入 C++23 的消息,networking 那就更无望了。
结合 C++11 之后两个补丁版本的更新内容看,2026 年能上线 networking 就不错了。

网络(15 年,仍在进行中)

15年,错过了 web2.0,错过了移动互联,错过了异步模型……

我注意到,2018 年秋天的会前邮件(新论文汇总)的字数是莎士比亚全集的三倍。

我之前抨击过,无端造出许多没有任何表征的讳莫高深的概念,然后又用大幅文字去定义、解释。最近几年,JavaScript 近乎以一年一版本的频率大幅更新;Python 也仅用了 10 年左右时间完成了 Python2 到 Python3 的完全过渡,甚至将 Python3 迭代到了 Python3.10。我也真纳闷,标委会的老爷们你们是怎么沉得住气的?!

我从温斯顿·丘吉尔的格言中得到些许安慰,“民主是最糟糕的政府形式,除了所有那些人类一再尝试过的其他形式”。

悔之晚矣……

4. C++11:感觉像是门新语言

直到 2018 年,我仍能看到 C++98 前的编译器被用于教学。我认为这是对学生的虐待,剥夺了他们接触学习我们 20 多年的进展的机会。

我的面试经历则给了我不同的感受:应届生们对 C++11 后的新标准的掌握程度普遍好于已工作多年的程序员。这算不算是一种自虐?

即使有了 C++11 的支持,我仍然认为无锁编程是专家级的工作。

我至今没有、可预见的未来也不会了解的几个 C++ 知识点之一。

我曾希望这会产生一个由线程池支持的工作窃取(work-stealing)的实现,但我还是失望了。

我父有些过分的仁慈、民主了。这么一个被普遍应用的技术方案,最后还是跟随了标委会的意愿。

我早在 1982/83 年冬天就实现了 auto,但是后来为了保持 C 兼容性而不得不移除了这一特性。

我父太难了…………既要对标委会妥协,又要为了市场份额不得不跟 C 保持双向兼容。

直到后来(大约从 2015 年开始),constexpr 函数才成为元编程的主要支柱。C++14 允许在 constexpr 函数中使用局部变量,从而支持了循环;在此之前,它们必须是纯函数式的。C++20(最终,在首次提出后约 10 年)允许将字面类型用作值模板参数类型 [Maurer 2012]。因此,C++20 将非常接近最初的目标(1979 年),即在可以使用内建类型的地方也都可以使用用户定义的类型。

关于 constexpr 的讨论是有史以来最激烈、最不愉快的。

lambda 表达式的实现基本上是编译器构建一个合适的函数对象并传递它。

变参模板的缺点是容易导致代码膨胀,因为 N 个参数意味着模板的 N 次实例化。

有些实现技巧实属“黑魔法”,不应当暴露给非专家。大部分程序员可以愉快地编写多年好的 C++ 代码,而不用了解这些复杂手段和神秘技巧。遗憾的是,初学者们一拥而上去研究这些最可怕的特殊代码,并从给别人(经常是错误地)解释它们的过程中得到巨大的自豪感。博主和演讲者们通过显摆令人提心吊胆的例子抬高他们的名望。这是 C++ 语言复杂性名声的一个主要来源。

面试没必要太刁。

random 库提供了分布函数和随机数产生器,其复杂性被誉为“每个随机数库都想长成的样子”。但它对初学者或者一般用户(常需要随机数)并不易用。(此处省略原文……)

相比之下,Howard Hinnant 的 chrono 库 [Hinnant et al. 2008] 处理时间点和时间间隔,在提供复杂功能的同时仍保持了易用性。

是的!两个库都用过,体验天差地别。

5. C++14:完成 C++11

5.1 数字分隔符

1
2
auto a = 1'234'567;    // 1234567(整数)
auto b = 1'234'567s; // 1234567 秒

5.2 变量模板

2012 年,Gabriel Dos Reis 提议扩展模板机制,在模板类、函数和别名 [Dos Reis 2012] 之外加入模板变量。例如:

1
2
template<typename T>
constexpr T pi = T(3.1415926535897932385);

5.3 函数返回类型推导

1
2
template<typename T>
auto size(const T& a) { return a.size(); }

5.4 泛型 lambda 表达式

1
auto get_size = [](auto& m){ return m.size(); };

5.5 constexpr 函数中的局部变量

允许使用局部变量和 for 循环。

1
2
3
4
5
6
7
8
9
10
constexpr int min(std::initializer_list<int> xs)
{
int low = std::numeric_limits<int>::max();
for (int x : xs)
if (x < low)
low = x;
return low;
}

constexpr int m = min({1,3,2,4});

评论