编程语言设计的最终目的,是在程序员交付有用的程序的同时,改进他们的思考方式和工作方式。尽管有些程序语言被视为“只是实验性的”,但是一旦程序语言被用于和语言本身无关的实际工作,这门语言的设计者们就应对他们的用户承担相应的责任。正确、合适、稳定性和足够的性能就成为重要的课题。对 C++ 来说,这些事情在 1979 年仅用了 6 个月就发生了。C++ 已经茁壮成长了 40 年之久。为什么能成功?又是如何成功的?
我之前的 HOPL 论文 [Stroustrup 1993, 2007] 以 1991 到 2006 年的观点回答了这些问题。从那时起发生的变化,除了语言的特性和组件库之外,主要是标准委员会的作用和影响(§3)。
这里,我主要考虑:
C++ 为高要求的应用而生,并成长为一种重要的编程语言——在某些领域,它甚至是主导语言。这是在没有认真的商业支持和没有营销的情况下达到的。许多现代语言拷贝了它的特性和理念。关键的语言技术贡献有:
- 静态类型系统,对内置类型和用户定义类型具有同等支持(§2.1)
- 既有值语义,又有引用语义(§4.2.3)
- 系统和通用资源管理(RAII)(§2.2)
- 支持高效的面向对象编程(§2.1)
- 支持灵活的和高效的泛型编程(§10.5.1)
- 支持编译期编程(§4.2.7)
- 直接使用机器和操作系统资源(§1)
- 通过库提供并发支持(往往使用内建函数实现)(§4.1)(§9.4)
相较于目前占主导地位的依靠垃圾收集器和广泛运行期支持的“托管”模式——典型的如 Java、C#、Python 和 JavaScript(§2.3)等语言——C++ 提供了一种不同的、对许多应用领域来说更好的软件模式。我所说的 “更好”是指更容易编写、更有可能正确、更可维护、使用更少的内存、耗能更低和更快。
这些贡献的领域是互帮互助的,举例来说:
- 引用语义(例如,指针和智能指针)支持使用值语义(例如,
jthread
和vector
)高效地实现高级类型。 - 对内置类型和用户定义类型的统一规则,简化了泛型编程(内置类型不是特殊情况)。
- 编译期编程使得一系列的抽象技术因为能够有效使用硬件而变得负担得起。
- RAII 允许使用用户定义的类型,而无需采取特定的操作来支持其实现对资源(包括非内存资源)的使用。
C++ 成功的根本原因很简单——它填补了编程领域的一个重要的“生态位”:
需要有效使用硬件和管理高复杂性的应用程序
如果你能负担得起“浪费”25% 甚至 99% 的硬件机能,那可供选择的编程语言和环境就多了。如果你的底层模块需要仅仅千行的底层代码,C 语言或者汇编语言可以效劳。40 年以来,C++ 的独特“生态位”足以使其社区不断成长。
这里有一个现代(2014 年)的 C++ 总结:
- 直接映射到硬件
- 指令和基本数据类型
- 最初来自于 C 语言
- 零开销抽象
- 带构造和析构函数的类、继承、泛型编程、函数对象
- 最初来自于 Simula 语言(当时还不是零开销的)
Simula 开创了许多抽象机制和一个灵活的类型系统,但在运行时间和空间成本上,它们带来了沉重的代价。与 1995 年的 C++(§2.1)描述相比,关注点从编程技术转向了问题领域。这更多的是解释风格和人们兴趣的不同,而不是语言设计的不同。这两个总结现在和当时都是准确的。
在过去几十年的基础上,21 世纪的关键技术进步包括:
- 内存模型(§4.1.1)
- 类型安全的并发支持:线程和锁(§4.1.2)、并行算法(§8.5)、合并线程(§9.4)
- 类型推导:
auto
(§4.2.1)、概念(§6)、模板参数推导(§8.1)、变参模板(§4.3.2) - 简化使用:
auto
(§4.2.1)、范围for
(§4.2.2)、并行算法(§8.5)、范围(§9.3.5)、lambda 表达式(§4.3.1) - 移动语义(§4.2.3)
- 编译期编程:
constexpr
(§4.2.7)、编译期循环(§5.5)、可确保的编译期求值和容器(§9.3.3)、元编程(§10.5.2) - 泛型编程:STL(§10.5.1)、概念(§6)、用户定义类型作为模板参数(§9.3.3)、lambda 表达式(§4.3.1)
- 元编程(§10.5.2)
它们都与零开销原则相关,但最后两个有点令人惊讶,因为在 2006 至 2020 年期间内,C++ 对它们的支持并不完全。
假如 C++ 分裂成互不兼容的方言,或者成为你无法长期依赖的东西,以上这些就都失去意义了:
新特性(C++11 以来)带来了标准库的改进(例如:unique_ptr
、chrono
、format
和 scoped_lock
),也带来了很多其他库的改进。
C++ 的目的是成为构建应用程序的工具,许多用 C++ 开发的伟大应用程序,例如在(§2.3)和(§10.1)章节提到的那些,是 C++ 真正的成功。
没有一种语言对所有人和所有事都是完美的。对于这点,没有人比既懂多种语言、又严肃使用其中一种并努力支持它的人了解更多了。阻碍进步的很少是单纯的无知。相反,重大改进的主要障碍是缺乏方向、缺乏开发资源以及害怕破坏现有代码。
C++ 苦于诞生过早,在现代化的集成开发环境(IDE)、构建系统、图形界面(GUI)系统和 Unicode 问世之前就已经诞生了。我期待 C++ 能慢慢赶上来。举例来说:
- 工具使用:从 C 语言开始,用字符和词法标记来说明语义,以及用
#include
和宏来组织源代码,这一直是有效工具建设的主要障碍。模块应该会有所帮助(§9.3.1),而且是有可能为 C++ 设计出一个合理的内部表示的 [Dos Reis and Stroustrup 2009, 2011]。 - 教育:今天的 C++ 教学大多仍然过时和落后(§2.3)。核心指南(§10.6)是对实践进行现代化的一种方法。WG21 的教育研究小组(§3.2)和许多面向教育的会议报告表明,这些问题得到了重视和并正在解决中。
- 打包和发布:C++ 诞生时,由独立开发、维护的模块组成的软件并不常见。今天,已经有了用于 C++ 的构建系统和打包管理程序。然而,还没有一个是标准的,有些难以用于简单的任务,有些则不够通用,不能应对使用 C++ 构建的大规模系统。我在 2017 年的 CppCon 主题演讲中提出了这个问题,并向社区发起挑战 [Stroustrup 2017c] 来解决它。我认为我们正在看到进展。此外,C++ 社区还缺少一个标准的地方来寻找有用的库。Boost [Boost 1998–2020] 是解决这个问题的一个努力,GitHub 正逐渐成为一个通用的资源库。但要达到让相对的新手能找到、下载、安装和运行几个主流的库这样的方便程度,我们的路还很长。
- 字符集和图形:C++ 语言和标准库依赖于 ASCII,但大多数应用程序使用某种形式的 Unicode。WG21 工作组现在有一个研究小组试图找到一个方式去标准化 Unicode 支持(§3.2)。缺乏标准的图形和图形界面则是更难的问题。
- 清理陈年烂账:这非常困难,而且令人不快。例如,我们知道内置类型之间的隐式窄化转换会导致无穷无尽的问题(§9.3.8),但是有数以万亿计的 C++ 代码行,这些代码以难以预测的方式依赖于那些转换。试图通过添加“更现代”的特性来替换旧特性来进行改进很容易成为 N+1 问题(§4.2.5)的牺牲品。改进的工具(例如静态程序分析和程序转换)提供了希望。
大型语言社区所面临的挑战是多种多样的,不可能有单一而简单的解决方案。这不仅仅是一个语法、类型理论或基本语言设计的问题。有些问题是商业性的。在工业规模上取得成功所需的各种技能范围令人望而生畏。时间会证明,C++ 社区是否能处理好所有这些问题,以及更多的其他问题。这点上我适度乐观,因为现在所有领域都已经有一些积极的举措(§3.2)。
C++ 是由一个大型委员会控制的,成员多种多样,并且会不断变化(§3.2)。因此,除了技术问题外,我们必须考虑在语言的演化过程中什么是有效的:
- 问题驱动:C++ 开发应该被那些真实世界中的具体问题的需求所驱动。
- 简单:C++ 应该从简单、高效、易用的解决方案中进行推广而成长。
- 高效:C++ 语言和标准库应该遵循零开销原则。
- 稳定性:不要搞砸我的代码!
大部分(全部?)C++ 最成功部分的开发都遵从了那些“经验法则”。它们自然会限制语言的发展范围,但这是好事。C++ 并不意味着对所有的人都是无所不能的。此外,这些原则迫使 C++ 在现实世界的挑战中相对缓慢地成长,并从反馈中受益。也请参见《C++ 语言的设计和演化》中的其他“经验法则” [Stroustrup 1994] 和我的 HOPL2 论文 [Stroustrup 1993]。这里面一直有连续性。
相比之下,一个功能如果设计时没有明确专注在解决大部分开发者实际面临的问题上,那它通常会失败:
- 只为专家:某个功能从开始的时候就要满足所有专家的需要。
- 模仿:我们需要这个功能,因为它在另外某个语言里很流行。
- 理论性:语言理论里说语言里一定要有这个特性。
- 革命性:此功能非常重要,以至于我们必须打破兼容性,或者摒弃那些不好的老方法。
我的结论是,尽早确定方向和期望至关重要。稍晚一些,就会有太多的人有太多的不同意见,因而无法达成一套连贯而一致的想法。
给定一个方向和一组原则,一种语言可以基于不同的工具来发展,如反馈、用户体验、实验和理论。这是好的工程方法;反之,则是无原则的实用主义或教条的理想主义。
C++ 标准委员会的章程几乎只关注语言和库的设计。这是有局限性的。一直以来,像动态链接、构建系统和静态分析之类的重要主题大多被忽略了。这是个错误。工具是软件开发人员世界的一个重要组成部分,要是能不把它们置于语言设计的外围就好了。
热衷于各种不同的想法具有危险性。在 2018 年的一篇论文 [Stroustrup 2018d] 中,我列出了 51 条最近的提案:
我列出了我认为有可能显著改变我们编写代码方式的论文,每一篇对教学、维护和编码指导都有重要的影响,其中许多对实现也有影响。
单独来说,许多(大多数)提案都是有道理的,但是放在一起却是疯狂的,甚至足以危及 C++ 的未来。
那篇论文的题目是《记住瓦萨号!》(Remember the Vasa!)。瓦萨号是 17 世纪瑞典的一艘宏伟战舰,由于设计上不断后期添加以及测试不充分,在首航时就沉没在斯德哥尔摩港。在 1990 年代,委员会经常提醒自己记得瓦萨号,但在 2010 年代,这一教训似乎已经被遗忘。
为了对委员会的流程进行组织约束,方向组提出 C++ 程序员的“权利法案” [Dawes et al. 2018]:
- 编译期稳定性:新版本标准中的每一个重要行为变化都可以被支持以前版本的编译器检测到。
- 链接期稳定性:除极少数情况外,应避免 ABI 兼容性破坏,而且这些情况应被很好地记录下来并有书面理由支持。
- 编译期性能稳定性:更改不会导致现有代码的编译时间开销有明显增加。
- 运行期性能稳定性:更改不会导致现有代码的运行时间开销有明显增加。
- 进步:标准的每一次修订都会为某些重要的编程活动提供更好的支持,或为某些重要编程群体提供更好的支持。
- 简单性:每一次对标准的修订都会简化某些重要的编程活动。
- 准时性:每一次标准的修订都会按照公布的时间表按时交付。
接下来的几十年,我们将会看到结果到底怎么样。
从近期来说,C++20 会像 C++11 那样,让 C++ 社区受益良多。在 2020 年 2 月的布拉格会议上,委员会对 C++20 进行了定稿,也投票同意了 Ville Voutilainen 的“C++23 大胆计划” [Voutilainen 2019b]:
“在 C++23 努力做到以下几点:”
注意关注点是在库上。“同时也在以下方面取得进展:”
鉴于这些议题的工作已经相当深入,委员会有可能会完成大部分工作。这一大群充满热情的人还能拿出什么东西并达成共识,就不那么容易预测了。对于未来几年,方向小组(我是其中的一员)提到了一些有希望进一步开展工作的领域 [Hinnant et al. 2020]:
- 改进 Unicode 的支持
- 支持简单图形和简单用户交互
- 支持新类型的硬件
- 探索错误处理的更好表达方式和实现方法
在委员会之外,我期望在构建系统、包管理和静态分析方面取得重大进展(§10.4)。
再往后的五年、十年或更远的未来,我在预测水晶球里就有点看不清了。在这个时间范围内,我们需要着眼于根本,而不是具体的语言特性。我希望标准委员会能注意到学到的教训(§11.4),并把重点放在根本上(§11.1):
- 把完全资源安全和类型安全的 C++ 作为追求目标
- 很好地支持各种各样的硬件
- 保持 C++ 的稳定性记录(兼容性)
保持稳定性需要在关注兼容性的同时,抵制试图通过添加大量“完美”特性来取代不完美或不时髦的旧方式来大幅改善 C++ 的冲动。新的特性总是会带来意外(有些令人愉快,有些则不那么令人愉快),旧的特性不会简单地消失。记住瓦萨号![Stroustrup 2018d](§11.4)。很多情况下,库、指南和工具是比修改语言更好的方法。
对于单线程计算来说,硬件已无法变得更快,所以对效率的重视将持续存在,而有效支持各种形式的并发和并行的压力将不断增加(§2.3)。专用硬件将大量涌现(例如,各种内存架构和特殊用途的处理器);这将使 C++ 这样的、可以利用这些硬件的语言受益。唯一比硬件性能增长更快的是人们的期望。
随着系统越来越复杂,开销可负担的抽象机制的重要性也在增加。对于依赖实时交互的系统,可预测的性能是至关重要的(例如,许多实时系统禁止使用自由存储(动态内存))。
随着我们对计算机化系统的依赖程度的增加、高手黑客数量的增多,安全问题只会越来越重要。为了防御,我看好硬件保护,看好更结构化、能支持更好的静态分析的系统,而非无休止的临时运行期检查和低级代码。
语言和系统之间的互操作性仍会至关重要;很少有大系统会只用一种语言来编写。
随着系统变得越来越复杂,对可靠性的要求也越来越高,对设计和编码质量的需求也急剧增加。我认为 C++ 已经为此做好了充分的准备,C++23 的计划是要进一步加强它。然而,仅靠语言特性是不足以满足未来需求的。我们需要有工具支持的使用指南,以确保语言的有效使用(§10.6)。特别是,我们需要确保完全的类型安全和资源安全,这必须反映在教育中。为了蓬勃发展,C++ 需要为新手提供更好的教育材料,也需要帮助有经验的程序员掌握现代 C++。仅仅介绍奇技淫巧和高级用法是不够的,而且反而会因为增强了 C++ 的复杂性名声而对语言造成伤害。
由于种种原因,我们需要简化大多数的 C++ 使用的场景。C++ 的演进已经使之成为可能,而我预计这一趋势将继续下去(§4.2)。改进的优化器——有能力利用代码中使用的类型系统和抽象——让优化这件事变得不同了。在过去的几年里,这极大地改变了我优化代码的方式。我从放弃精巧而复杂的东西开始,那是错误的藏身之处;并且,如果我难以理解发生了什么,编译器和优化器也会如此。我发现,这种方法通常会给我带来从适度到惊人的性能提高,同时也简化了未来的维护。只有当这种方法不能给我带来我想要的性能时,我才会求助于高级(又称复杂)的数据结构和算法。这是 C++ 抽象机制设计上的一大胜利。
我期待着看到用 C++ 构建更多令人兴奋的应用程序,并看到新的编程惯用法和设计技巧的发展。
我也希望其他语言能从 C++ 的成功中学习。假如从 C++ 的演化中吸取的经验教训仅局限于 C++ 社区,那将是可悲的。我希望并期待在其他语言和系统中看到 C++ 模型的关键方面,这将是一个真正的成功衡量标准。在一定程度上,这已经发生了(§2.4)。
我痛苦地意识到
- 这篇论文太长了。
- 对大多数技术主题的描述都省略了很多可以看作是根本的内容。很多情况下,许多人经年累月的工作会被简化为一页甚至一句话。特别是,我忽略了并发性这个极其重要的话题;它应该有一篇专门的长篇论文来进行详述。
感谢让 C++ 成功的数以百万计的程序员,他们创建的应用是我们这个世界的关键部件。
感谢本文草稿的审稿人,包括 Al Aho、A. Bratterud、Shigeru Chiba、J. Daniel Garcia、Brent Hailpern、Howard Hinnant、Roger Orr、Aaron Satlow、Yannis Smaragdakis、David Vandevoorde、J.C. Van Winkel 和 Michael Wong。本文的完整性和准确性在很大程度上依靠这些审稿人。当然,错误归我自己。
感谢 Guy Steele 帮我顺利解决了 LaTex 和 BibTex 中的谜团,把文章引用做到满足 ACM 要求的形式。
感谢所有在标准上努力工作的人。还有很多我没有提到的名字,可以在 WG21 论文的作者和这些论文的致谢部分中找到。我参考和引用的许多“P”和“N”编号的论文保存在 open-std.org/jtc1/sc22/wg21/docs/papers/。没有这些论文,本文的一些内容就会过度依赖我的记忆了。