C++ 2006–2020

 主页   资讯   文章   代码   电子书 

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

C++ 从 1979 年到 2006 年的历史记录在我的 HOPL 论文中 [Stroustrup 1993, 2007]。在那段时间里,C++ 从一个单人的研究项目成长为大约有 300 万程序员的社区。

2.1 第一个十年

后来成为了 C++ 的东西始于 1979 年 4 月,名为带类的 C。我的目标是设计一个工具,它既拥有直接而高效的处理硬件的能力(例如编写内存管理器、进程调度器和设备驱动程序),又同时可以有类似 Simula 的功能来组织代码(例如“强”静态可扩展类型检查、类、多级类和协程)。我想用这个工具编写一版 Unix 内核,可以在通过局域网或共享内存互联的多个处理器上运行。

我选择 C 作为我工作的基础,因为它足够好,并且在办公室里就能得到很好的支持:我的办公室就在 Dennis Ritchie 和 Brian Kernighan 走廊对面。然而,C 语言并不是我考虑的唯一语言。Algol68 当时深深吸引了我,我还是 BCPL 和其他一些机器层面的语言的专家。C 后来的巨大成功在当时还完全不确定,但是 Brian Kernighan 和 Dennis Ritchie 杰出的介绍和手册 [Kernighan and Ritchie1978] 已经出现,Unix 也正开始它的胜利路程。

最初我实现的是一个预处理器,它将“带类的 C” 差不多逐行翻译成 C。1982 年,在“带类的 C”的用户数量增长到了几十人的时候,这种方法已经显得无法把控了。所以我写了一个传统的编译器,叫作 Cfront,1983 年 10 月第一次给别人使用。Cfront 是一个传统的编译器,它有一个词法分析器、一个构建抽象语法树的语法分析器、一个用类型装饰语法树的类型检查器,以及一个重新排列 AST 以提高生成代码的运行期效率的高层次优化器。关于 Cfront 的本质有很多困惑,因为当时它最终输出的是 C(优化的,不是特别可读的 C)。我生成了 C,这样我就不必直接处理当年正在使用的众多的(非标准化)链接器和优化器。不过,Cfront 一点也不像传统的预处理器。你可以在计算机历史博物馆的源代码收藏 [McJones 2007–2020] 中找到一份带有文档的 Cfront 源代码。Cfront 从“带类的 C”自举为 C++,所以第一个 C++ 编译器是用(简单的)C++ 写的,适合非常小的计算机(内存小于 1MB,处理器速度小于 1MHz)。

“带类的 C” 添加到 C 上的第一个特性是类。我从早期在 Simula 中的使用中了解到它们的力量,在 Simula 中,类是严格静态、但又可扩展的类型系统的关键。我立即添加了构造函数和析构函数。它们当时非常新颖,但从我的计算机架构和操作系统背景来看,我认为它们也不算很新奇,因为我需要一个机制来建立一个工作环境(构造函数)和一个逆操作来释放运行期获得的资源(析构函数)。以下摘自我 1979 年的实验记录本:

  • “new 函数”为成员函数创建运行的环境
  • “delete 函数”则执行相反的操作

“new 函数”和“delete 函数”这两个术语是“构造函数”和“析构函数”的原始术语。直到今天,我仍然认为构造函数和析构函数是 C++ 的真正核心。另见(§2.2.1)和(§10.6)。

当时,除了 C 语言,基本上所有语言都有适当的函数参数类型检查。我认为没有它我无法完成任何重要的事情。因此,在我的部门主管 Alexander Fraser 的鼓励下,我立即添加了(可选的)函数参数声明和参数检查。这就是 C 语言中现在所说的函数原型。1982 年,在看到让函数参数检查保持可选的效果后,我将其设为强制的。这导致了十几二十年里关于与 C 不兼容的大声抱怨。人们想要保留他们的类型错误,或者至少许多人大声说他们不想检查,并以此作为不使用 C++ 的借口。这个小事实也许能让人们认识到,演化一门被大量使用的语言所涉及到的各种问题。

鉴于过于狭隘的 C 和 C++ 爱好者之间偶尔会恶语相向,或许值得指出,我一直是 Dennis Ritchie 和 Brian Kernighan 的朋友,在 16 年里几乎天天同他们一起吃午饭。我从他们那里学到了很多,现在还经常同 Brian 见面。我将一些对 C++ 语言的贡献 [Stroustrup 1993] 归功于他们两位,而我自己也是 C 的主要贡献者(例如函数定义语法、函数原型、const// 注释)。

为了能够理性思考 C++ 的成长,我想出了一套设计规则。这些在 [Stroustrup 1993, 1994] 中有介绍,所以这里我只提一小部分:

  • 不要陷入对完美的徒劳追求。
  • 始终提供过渡路径。
  • 说出你的意图(即,能够直接表达高层次的思路)。
  • 不要隐式地在静态类型系统方面违规。
  • 为用户定义类型提供和内置类型同样好的支持。
  • 应取消预处理器的使用。
  • 不要给 C++ 以下的低级语言留有余地(汇编语言除外)。

这些并不是没有雄心壮志的目标。其中某些目标,现在 2020 年了我依然在为之努力工作。在 1980 年代早期到中期,我给 C++ 添加了更多的语言功能:

  • 1981 年const——支持接口和符号常量的不变性。
  • 1982 年:虚函数——提供运行期多态。
  • 1984 年:引用——支持运算符重载和简化参数传递。
  • 1984 年:运算符和函数重载——除了算法和逻辑运算符外,还包括:允许用户定义 =(赋值)、()(调用;支持函数对象(§4.3.1))、[](下标访问)和 ->(智能指针)。
  • 1987 年:类型安全链接——消除许多来自不同翻译单元中不一致声明的错误。
  • 1987 年:抽象类——提供纯接口。

在 1980 年代后期,随着计算机能力的急剧增强,我对大型软件更感兴趣,并做了如下补充:

  • 模板——在经历了多年使用宏进行泛型编程的痛苦之后,更好地支持泛型编程。
  • 异常——试图给混乱的错误处理带来某种秩序;RAII(§2.2.2)便是为此目标而设计的。

后面这些功能并没有受到普遍欢迎(例如,见(§7))。部分原因是社区已经变得庞大和难以管理。ANSI 标准化已经开始,所以我不再能够私下实现和实验。人们坚持大规模的精心设计,坚持在认真实施之前进行广泛的辩论。我不能再在明知道不可能让每个人都满意的情况下,从一个最小的提议开始,把它发展成一个更完整的功能。例如,人们坚持到处使用笨重的带有 template<class T> 前缀的模板语法。

在 1980 年代末,“面向对象”的宣传变得震耳欲聋,淹没了我对 C++ 传达的讯息。我对 C++ 是什么和应当成为什么的看法被广泛忽视了——很多人甚至从未听说过。对于“面向对象”的某些定义来说,所有新语言都应是“纯面向对象的”。“不真正面向对象”被视为是糟糕的,不容争辩。

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

C++ 是一门偏向系统编程的通用编程语言,它是

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

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

2.2 第二个十年

ANSI C++ 委员会是 1989 年 12 月在华盛顿特区的一次会议上成立的,距离第一次使用“带类的 C”这个名称仅仅 10 年多的时间。大约有 25 名 C++ 程序员出席了会议。我出席了会议,还有另外一些近些年来依然活跃的 ISO C++ 标准委员会成员当时也在。

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

形成 C++98 的工作是 HOPL3 论文的核心 [Stroustrup 2007],所以这里我只简单总结一下。

2.2.1 语言特性

C++98 的主要语言特性是

  • 模板——无约束的、图灵完备的、对泛型编程的编译期支持,在我早期工作(§2.1)的基础上进行了许多细化和改进;这项工作仍在继续(§6)。
  • 异常——一套在单独(不可见的)路径上返回错误值的机制,由调用方栈顶上的“在别处”的代码处理;见(§7)。
  • dynamic_casttypeid——一种非常简单的运行期反射形式(“运行期类型识别”,又名 RTTI)。
  • namespace——允许程序员在编写由几个独立部分组成的较大程序时避免名称冲突。
  • 条件语句内的声明——让写法更紧凑和限制变量作用域。
  • 具名类型转换——(static_castreinterpret_castconst_cast):消除了 C 风格的类型转换中的二义性,并使显式类型转换更加显眼。
  • bool:一种被证明非常有用和流行的布尔类型;C 和 C++ 曾经使用整数作为布尔变量和常量。

让我们看一个简单的 C++98 例子。dynamic_cast 是面向对象语言中常被称为类似“是某种”的概念的 C++ 版本:

void do_something(Shape* p)
{
    if (Circle* pc = dynamic_cast<Circle*>(p)) { // p 是某种 Circle?
        // ... 使用 pc 指向的 Circle ...
    }
    else {
        // ... 不是 Circle,做其他事情 ...
    }
}

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

一种更简单的变种是使用引用而不是指针:

void do_something2(Shape& r)
{
    Circle& rc = dynamic_cast<Circle&>(r);  // r 是某种 Circle!
    // ... 使用 rc 引用的 Circle ...
}

这简单地断言 r 指代一个 Circle,如果不是则抛出一个异常。思路就是,错误能够在本地被合理地处理时,使用指针和测试,如果不能则依赖引用和异常。

C++98 中最重要的技术之一是 RAII(Resource Acquisition Is Initialization, 资源获取即初始化)。那是我给它取的一个笨拙的名字,想法就是每个资源都应该有一个所有者,它由作用域对象表示:构造函数获取资源、析构函数隐式地释放它。这个想法出现在早期的“带类的 C ”中,但直到十多年后才被命名。这里有一个我经常使用的例子,用来说明并非所有资源都是内存:

void my_fct(const char* name)  // C 风格的资源管理
{
    FILE* p = fopen(name, "r");  // 打开文件 name 来读取
    // ... 使用 p ...
    fclose(p);
}

问题是,如果(在 fopen()fclose() 的调用之间)我们从函数 return 了,或者 throw 了一个异常,或者使用了 C 的 longjmp,那么 p 指向的文件句柄就泄漏了。文件句柄泄漏会比内存泄漏更快地耗尽操作系统的资源。这个文件句柄是非内存资源的一个例子。

解决方案是将文件句柄表示为带有构造函数和析构函数的类:

class File_handle {
    FILE* p;
public:
    File_handle(const char* name,const char* permissions);  // 打开文件
    ~File_handle();  // 关闭文件
    // ...
};

我们现在可以简化我们的用法:

void my_fct2(const char* name)  // RAII 风格的资源管理
{
    File_handle p(name,"r");    // 打开文件 name 来读取
    // ... 使用 p ...
} // p 被隐式地关闭

随着异常的引入,这样的资源句柄变得无处不在。特别的,标准库文件流就是这样一个资源句柄,所以使用 C++98 标准库,这个例子变成:

void my_fct3(const string& name)
{
    ifstream p(name);    // 打开文件 name 来读取
    // ... 使用 p ...
} // p 被隐式的关闭

请注意,RAII 代码不同于传统的函数使用,它允许在库中一劳永逸地定义“清理内存”,而不是程序员每次使用资源时都必须记住并显式编写。至关重要的是,正确和健壮的代码更简单、更短,并且至少与传统风格一样高效。在接下来的 20 年里,RAII 已遍布 C++ 库。

拥有非内存资源意味着垃圾收集本身不足以进行资源管理。此外,RAII 加上智能指针(§4.2.4)消除了对垃圾收集的需求。另见(§10.6)。

2.2.2 标准库组件

C++98 标准库提供了:

  • STL——创造性的、通用的、优雅的、高效的容器、迭代器和算法框架,由 Alexander Stepanov 设计。
  • 特征(trait)——对使用模板编程有用的编译期属性集(§4.5.1)。
  • string——一种用于保存和操作字符序列的类型。字符类型是一个模板参数,其默认值是 char
  • iostream——由 Jerry Schwartz 和标准委员会精心制作,基于我 1984 年的简单的数据流,处理各种各样的字符类型、区域设置和缓冲策略。
  • bitset——一种用于保存和操作比特位集合的类型。
  • locale——用来处理不同文化传统的精致框架,主要与输入输出有关。
  • valarray——一个数值数组,带有可优化的向量运算,但遗憾的是,未见大量使用。
  • auto_ptr——早期的代表独占所有权的指针;在 C++11 中,它被 shared_ptr(共享所有权)和 unique_ptr(独占所有权)(§4.2.4)替代。

毫无疑问,STL 框架是最为重要的标准库组件。我认为可以说,STL 和它开创的泛型编程技术挽救了 C++,使它成长为一种有活力的现代语言。像所有的 C++98 功能一样,STL 在其他地方已经有了广泛的描述(例如 [Stroustrup 1997, 2007]),所以在这里我只会给出一个简单的例子:

void test(vector<string>& v, list<int>& lst)
{
    vector<string>::iterator p
        = find_if(v.begin(), v.end(), Less_than<string>("falcon"));
    if (p != v.end())  {  // p 指向 'falcon'
        // ... 使用 *p ...
    }
    else {                // 没找到 'falcon'
        // ...
    }

    vector<int>::iterator q
        = find_if(lst.begin(), lst.end(), Greater_than<int>(42));
    // ...
}

标准库算法 find_if 遍历序列(由 begin/end 定界)寻找谓词为真的元素。该算法在三个维度上都是通用的:

  • 序列元素的存储方式(这里是 vectorlist
  • 元素的类型(这里是 stringint
  • 用于确定何时找到元素的谓词(此处为 Less_thanGreater_than

注意这里没有用到任何面向对象的方法。这是依赖模板的泛型编程,有时也被称为编译期多态。

模板的写法仍然很原始,但是从 2017 年左右开始,我可以使用 auto§4.2.1)、范围(§9.3.5)和 lambda 表达式(§4.3.1)来简化代码:

void test2(vector<string>& v, list<int>& lst)
{
    auto p = find_if(v,[](const string& s) { return s<"falcon"; })
    if (p!=v.end()) {
        // ...
    }
    // ...
    auto q = find_if(lst,[](int x) { return x>42; })
    if (q!=lst.end()) {
        // ...
    }
    // ...
}

2.3 C++ 的 2006

2006 年,我和 ISO C++ 委员会的大多数其他成员都对功能丰富的 C++0x 标准寄予厚望。计划在 2007 进行特性冻结,所以我们有一个合理的预期,C++0x 将是 C++08 或 C++09。事实上 C++0x 变成了 C++11,引出了关于十六进制 C++0xB 的笑话。

在我 2006 年的 HOPL 论文 [Stroustrup 2007] 中,我列出了 39 个提案,并预测前 21 个会进入 C++0x。有趣的是,我列表上的前 25 个建议中,有 24 个进入了 C++11。我把提案 22–25 列为“正在制定中,目标是在 2007 年 7 月进行投票”。令我惊喜的是,它们全都成功了。而提案 26–39 则连 C++17 都没有进入。这中间就留下了第 10 号提案“概念”,它有一个自己的长长的悲伤故事,不过最终还是以进入 C++20 而快乐收尾。

我和其他许多人对 C++0x 的延迟感到沮丧,并担心在面对来自更现代、资金更充足的替代品的竞争时,一个未经改进的 C++ 可能无法作为一种活的语言生存下去。在 2006 年,Java 的使用仍在增加,微软的 C# 也有大量的支持和营销。我在 2006 年的估计是 C++ 的使用在过去 4 年中首次略有下降。获取真实的数字很难,我的最佳估计(下降 7%)完全在误差范围内,但确实有理由去担心。类似 Java 和 C# 这样的语言会作出这样一种假设,并常常大声宣扬:C++ 没有生态位:

  • “低级编程”可以由少量的 C 或汇编代码处理。
  • “高级编程”则可以使用一种带有巨大的运行时支持系统的更安全、更小并使用垃圾收集的语言来做,这样可以更好、更便宜、更高效地完成。
  • 像 Java 和 C# 这样的托管语言使用垃圾收集和一致的运行期范围检查,使得不太专业的程序员能更有生产力,这样可以减少对高技能的开发人员的需求。
  • 编程语言与平台的深度集成,并使用集成工具来进行支持,这对生产力和大型系统的构建至关重要。

显然,我和许多其他人并不同意。但这些在过去和现在都是严肃的争辩,它们如果正确的话应该导致 C++ 被放弃使用。C++ 基于传统的编程语言模型,与底层操作系统分离,并由众多独立的工具供应者提供支持。托管语言往往是专有的;只有一个庞大而富有的组织才能开发所需的庞大基础设施和库。我和 C++ 社区中的其他许多人更喜欢不受公司控制的语言;这是我参加 ISO 标准工作的一个原因。

回想起来,2006 年可能是 C++ 的最低谷,但重要的技术事件也恰好在此时发生了:大约在 2005 年,历史上第一次单个处理器(单核)的性能停止提高,能效(“每瓦特的性能”)成为一个关键指标(尤其是对于服务器集群和手持设备)。计算经济学转而青睐更好的软件。硬件的进步再也不能完全掩盖语言或编程技术的低效。这样,执掌“利器”的高手跟差点的程序员或受工具链开销束缚的程序员相比,能赢得高一个数量级的经济优势,而这种优势十多年之后还依然存在。即使在今天,这些事实还没有被所有的教育和管理体制充分领会,但是现在有许多重要的任务,为它们花时间精心打造高性能的代码会获得巨大的回报。

另一个转折点来自供应商,他们试图通过定义标准接口(比如图形用户界面)将自己喜欢的语言强加给所有用户,而这只能通过使用他们喜欢的、通常是专有的语言来实现。比如谷歌对安卓系统使用 Java,苹果对 iOS 使用 Objective-C,微软对 Windows 使用 C#。应用程序供应商可以尝试通过使用一些编程方言来避开锁定,例如 Objective C++ [Objective C++ Wikipedia 2020] 或 C++/CLI [ECMA International 2005],但是这样写出的代码仍然不可移植。许多组织,比如如 Adobe、谷歌和微软,他们的响应方式是使用 C++ 编写他们要求苛刻的应用程序的主要部分,然后为各种平台(如 Android、iOS 和 Windows)使用薄接口层。2006 年时这一趋势几乎不引人注目。

在便携式设备(尤其是智能手机)上,对能效和平台独立性的需求是彼此融合的。一个影响是,据我在 2018 年的最佳估计,自 2006 年以来 C++ 程序员的数量增长了约 50%,达到约 450 万名开发人员 [Kazakova 2015]。也就是说开发者每年增长 15 万人,十年来每年大约增长 4%。

2006 年,很少有人注意到硬件趋势对 C++ 固有优势的滋养。而社区和标准委员会正在关注新的语言特性和库,以增加 C++ 的实用性并提高对它的热情。包括我在内的一些委员感到迫切需要重大改进。其他人更关注于稳定语言和改进它的实现。一个标准委员会需要这两个群体,但创新和整顿之间不断的拉锯战是紧张的来源。就像在任何大型组织中一样,维护现状和服务当前用户的人有组织上优势。在《C++ 程序设计语言(第三版)》[Stroustrup 1997] 中,我引用了尼科洛·马基雅维利(Niccolò Machiavelli)的话:

没有什么比开创一种新秩序更难于推行、更让人怀疑能否成功、处理起来更加危险。因为改革者会与所有从旧秩序中获利的人为敌,而所有从新秩序中获利的人却只是冷淡的捍卫者。

我的观点是 C++ 需要显著的改进来更好地服务于它的用户群体。C++ 应用程序被大规模部署,但是新项目通常选择更流行的语言,一些成功的 C++ 项目被改写成这样的语言。举例来说,谷歌的许多大规模应用,如搜索,一直是基于他们的 map-reduce 框架 [Dean and Ghemawat 2004, 2008]。它就是 C++ 程序。然而,由于它因为商业原因是专有的,人们复制了它,而开源的 map-reduce 框架(Hadoop)出于各种原因是用 Java 实现的。这对于 C++ 社区来说是一件憾事。

开发转向其他语言的另一个重要原因是,模板提供的接口的灵活性使得使用所有 C++ 特性并提供稳定的 ABI 变得极其困难:可以灵活,也可以提供稳定的二进制接口,但大多数组织都做不到两者兼顾。我认为人们之所以需要 C++ 编写的程序提供 C、Java、C# 之类的接口,这是个促成因素。C++ 的 ABI 稳定性是一个真正的技术难题,尤其是因为 C++ 标准必须独立于平台。

除了 C++ 社区的问题,到 2006 年,随着纸质出版的减少以及记者们关注流行技术和广告收入,大多数涉及 C++ 的专业软件杂志已经死亡。Dr. Dobbs 期刊又持续了几年(2009 年 2 月停刊)。C++ 会议被吸收到“面向对象”或一般软件开发会议中,剥夺了 C++ 社区展示新发展的场所。书籍仍在编写中,但程序员阅读的书籍越来越少(或至少购买的书越来越少,因为盗版变得越来越容易,因此统计数据变得越来越不可靠),在线资源变得越来越受欢迎。

一个更严重的问题是 C++ 在教育中的作用正在急剧下降。C++ 不再是“新的、有趣的”,而 Java 正作为一种更简单、更强大的语言被直接推向大学。美国高中计算机科学考试突然从 C++ 变成了 Java。在大学里,Java 作为入门语言的使用急剧增加。C++ 的教学质量也在下降,大多数课程优先选择 C 语言,或者认为严重依赖类层次结构的面向对象编程是唯一正确的方法。这两种方法都弱化了 C++ 的优势,并且需要大量使用宏。标准库(依靠泛型编程;(§2.2))和 RAII(依赖构造函数/析构函数对(§2.2.1))经常被完全排除在基础课程之外,或者被放在一个所谓的“高级特性”部分,大多数学生要么从未接触过,要么认为它很可怕。教科书经常陷入晦涩难懂的细节。当然也有例外,但平均来说,呈现给学生的 C++ 远不是最佳的工程实践。在 2005 年,我接受了挑战,给大学一年级的学生教编程。我调查了大约二十本最流行的 C++ 编程教材,最后大声抱怨:

如果那就是 C++,我也会不喜欢它!

在用一本著名的教科书教了一年书后,我开始只用自己的教案,并且在 2008 年出版了《C++ 程序设计:原理与实践》(Programming: Principles and Practice Using C++)[Stroustrup 2008a],但直到今天,许多 C++ 教学仍带有 1980 年代的特色。

尽管如此,C++ 的使用又开始增加了。我认为这是因为根本的技术趋势再次青睐 C++,并且在二十一世纪的第一个十年结束的时候,C++11 的出现也有所帮助。

Boost 库和 Boost 组织非常重要 [Boost 1998–2020]。1998 年,经验丰富的开发者及 WG21 的有影响力的成员 Beman Dawes 建立了一个“C++ 代码库网站”[Dawes 1998],其明确目标是开发 C++ 库以确立现有实践,使得未来的标准化可以据此进行。在此之前,C++ 甚至从来没有一个公共的代码库。Boost 慢慢成长为一个活跃的组织,有新库的同行评审和一年一度的会议。Boost 库被广泛使用,最流行的被吸收到标准中(例如,regex§4.6)、thread§4.1.2)、shared_ptr§4.6)、variant§8.3)和文件系统(§8.6))。对于 C++ 社区来说重要的是,Boost 库比它们的 ISO 标准版本早十多年,但仍被当作某种“预备标准”来信任。有许多委员会成员都参与了 Boost,特别是 Dave Abrahams、Doug Gregor、Jaakko Järvi、Andrew Sutton,当然还有 Beman Dawes。

到 2006 年,C++ 在业界已经不再是新鲜刺激的东西,但它遍布很多行业。在 C++ 诞生的电信行业,它一直被大量使用。从电信领域出发,它已经扩展到游戏(如 Unreal、PlayStation、Xbox 和 Douglas Adams 的《宇宙飞船泰坦》)、金融(如摩根士丹利和 Renaissance Technologies)、微电子(如英特尔和 Mentor Graphics)、电影(如皮克斯和 Maya)、航空航天(如洛克希德·马丁和美国国家航空航天局)和许多其他行业。

就我个人而言,我特别喜欢 C++ 在科学和工程中的广泛使用,比如高能物理(例如 CERN 欧洲核子研究中心、SLAC 国家加速器实验室、费米实验室)、生物学(例如人类基因组项目)、空间探索(例如火星漫游车和深空通信网络)、医学和生物学(例如断层扫描、常规成像、人类基因组项目和监控设备)等等。

2.4 其他语言

人们常常会寻找其他编程语言对 C++ 的直接技术影响。其实非常之少。典型情况是,影响涌现自共同的前代语言和共同思想(而非特定的现有语言)。扩展 C++ 的决定性理由往往与 C++ 社区中已经发现的问题有关。直接从流行语言中借鉴的情况并不常见,而且比人们想象的要困难得多。大多数标准委员会成员都掌握多种语言,并密切留意(其他语言中)有用的功能、库和技巧。

下面是其他语言在二十一世纪对 C++ 的某些真实或假想的影响:

  • auto——从初始化器推断类型的能力。它在现代语言中很流行,但也已由来已久。我不知它的最早起源,但我在 1983 年实现这个功能的时候,也并不认为它很新颖(§4.2.1)。
  • tuple——许多语言,特别是源自函数式编程传统的语言,都有元组,它通常是一个内置类型。C++ 标准库 tuple 及其许多用法都从中受到启发。std::tuple 派生自 boost::tuple [Boost 1998–2020](§4.3.4)。
  • regex——加入 C++11 的标准库 regex 是(经由 Boost;已致谢)从 Unix 和 JavaScript 的功能中拷贝来的(§4.6)。
  • 函数式编程——函数式编程特性和 C++ 构造之间有许多明显的相似之处。大多数不是简单的语言特性,而是编程技巧。STL 受到函数式编程的启发,并首先在 Scheme [Stepanov 1986] 和 Ada [Musser and Stepanov 1987] 中进行了尝试(未成功)。
  • futurepromise——源自 Multilisp,经由其他 Lisp 方言(§4.1.3)。
  • 范围 for——许多语言中都有对应物,但直接启发来自 STL 序列(§4.2.2)。
  • variantanyoptional——显然受到多种语言的启发(§8.3)。
  • lambda 表达式——显然,部分灵感来自于函数式语言中 lambda 表达式的应用。但是,在 C++ 中,lambda 表达式的根源还可以上溯到 BCPL 语言中用作表达式的代码块、局部函数(多次被 C 和 C++ 拒绝,因其容易出错且增加了复杂性)和(最重要的)函数对象(§4.3.1)。
  • finaloverride——用于更明确地管理类层次结构,并且在许多面向对象的语言中都可以使用。在早期的 C++ 中已经考虑过它们了,但当时被认为是不必要的。
  • 三向比较运算符 <=>,受 C 的 strcmp 及 PERL、PHP、Python 和 Ruby 语言的运算符的启发(§9.3.4)。
  • await——C++ 里最早的协程(§1.1)受 Simula 启发,但是作为库提供,而不是作为语言特性,这是为了给其他替代的并发技术留出空间。C++20 中的无栈协程的思想主要来自 F#(§9.3.2)。

即使以非常直接的方式从另一种语言借用了某个特性,该特性也会发生变化。通常,为了适合 C++ 语法会发生很大变化。当从支持垃圾收集的语言借鉴时,生命周期问题必须得到解决。而 C++ 区分对象和对象的引用,这通常使得 C++ 需要以和原始语言不同的方式来解决。在“翻译”成 C++ 的过程中,经常会发现全新的用法。在把 lambda 引入 C++ 的过程中,出现了大量的此类现象的事例(§4.3.1)。

在很多人的想象中,我(和其他参与 C++ 的人)整日无所事事,满脑子想的是在流行语言中如何占据主导地位,为这个复杂的语言战争制定战略。实际上,我并没有在那上面花时间。大多数日子里,我不会去思考其他的语言,除非我碰巧出于一般的技术兴趣去学习一门其他语言或要使用它来完成一些工作。我要做的是与软件开发人员交谈,考虑人们在使用 C++ 时遇到的问题,还要考虑潮水般涌入标准委员会的改进建议。当然,我也编写代码来体验问题并测试改进思路。问题在于要能抽出时间冷静地考虑,什么是根本的,什么只是一时流行,以及什么会造成危害。

同样,C++ 对其他语言的贡献也难以估量。通常,类似的特性是平行演化的,或有着共同的根源。例如:

  • Java 和 C# 中的泛型——他们采用了其他语言的泛型模式,但采用了 C++ 语法,并且是在 C++ 大规模展示了泛型编程的用途之后,才添加泛型。
  • Java、Python 等的资源弃置惯用法(dispose idiom)——这大致是在垃圾收集语言中最能接近析构函数的做法了。
  • D 编程语言进行编译期求值——我向 Walter Bright 解释了早期的 constexpr 设计。
  • C++ 基于构造函数和析构函数的对象生存期模型是 Rust 灵感的一部分。好笑的是,最近 C++ 经常被指责从 Rust 那里借用了这种想法。
  • C 采用了 C++11 的内存模型、函数声明和定义语法、以声明为语句、const// 注释、inline 以及 for 循环中的初始化表达式。

C++ 与其他语言之间的许多差异源于 C++ 对析构函数的使用。这使得垃圾收集的语言很难直接从 C++ 借用。