Professional Documents
Culture Documents
02
02
背景:C++ 的 1979–2006
## 2.1 第一个十年
当时,除了 C 语言,基本上所有语言都有适当的函数参数类型检查。我认为没有它我无法完成任何重要的事情。
因此,在我的部门主管 Alexander Fraser 的鼓励下,我立即添加了(可选的)函数参数声明和参数检查。这
就是 C 语言中现在所说的函数原型。1982 年,在看到让函数参数检查保持可选的效果后,我将其设为强制的。
这导致了十几二十年里关于与 C 不兼容的大声抱怨。人们想要保留他们的类型错误,或者至少许多人大声说他们
不想检查,并以此作为不使用 C++ 的借口。这个小事实也许能让人们认识到演化一门被大量使用的语言会涉及
到的各种问题。
鉴于过于狭隘的 C 和 C++ 爱好者之间偶尔会恶语相向,或许值得指出,我一直是 Dennis Ritchie 和
Brian Kernighan 的朋友,在 16 年里几乎天天同他们一起吃午饭。我从他们那里学到了很多,现在还经常
同 Brian 见面。我将一些对 C++ 语言的贡献 [Stroustrup 1993] 归功于他们两位,而我自己也是 C 的
主要贡献者(例如函数定义语法、函数原型、`const` 和 `//` 注释)。
> - 不要陷入对完美的徒劳追求。
> - 始终提供过渡路径。
> - 说出你的意图(即,能够直接表达高层次的思路)。
> - 不要隐式地在静态类型系统方面违规。
> - 为用户定义类型提供和内置类型同样好的支持。
> - 应取消预处理器的使用。
> - 不要给 C++ 以下的低级语言留有余地(汇编语言除外)。
- **1981 年**:`const`——支持接口和符号常量的不变性。
- **1982 年**:虚函数——提供运行期多态。
- **1984 年**:引用——支持运算符重载和简化参数传递。
- **1984 年**:运算符和函数重载——除了算术和逻辑运算符外,还包括:允许用户定义 `=`(赋值)、`()
`(调用;支持函数对象([§4.3.1](04.md#431-lambda-表达式)))、`[]`(下标访问)和 `->`(智能
指针)。
- **1987 年**:类型安全链接——消除许多来自不同翻译单元中不一致声明的错误。
- **1987 年**:抽象类——提供纯接口。
在 1980 年代后期,随着计算机能力的急剧增强,我对大型软件更感兴趣,并做了如下补充:
- 模板——在经历了多年使用宏进行泛型编程的痛苦之后,更好地支持泛型编程。
- 异常——试图给混乱的错误处理带来某种秩序;RAII([§2.2.1](#221-语言特性))便是为此目标而设计的。
后面这些功能并没有受到普遍欢迎(例如,见([§7](07.md#7-错误处理)))。部分原因是社区已经变得庞大
和难以管理。ANSI 标准化已经开始,所以我不再能够私下实现和实验。人们坚持大规模的精心设计,坚持在认
真实施之前进行广泛的辩论。我不能再在明知道不可能让每个人都满意的情况下,从一个最小的提议开始,把它
发展成一个更完整的功能。例如,人们坚持到处使用笨重的带有 `template<class T>` 前缀的模板语法。
我从未使用过“C++ 是一种面向对象的编程语言”这种说法,这件事很多人并不知道,或者因为感到有些尴尬而
有意忽略了。那时候,我的标准描述是
这个说法过去和现在都是准确的,但不如“万物皆对象!”这样的口号那么令人兴奋。
## 2.2 第二个十年
经过了惯例性的、大约十年的工作,该委员会终于发布了第一个标准:C++98。我和许多其他人自然更愿意更快
地输出一个标准,但是委员会规则、过度的雄心和各种各样的延迟使我们在时间表方面与 Fortran、C 和其他
正式标准化的语言站在了同一起跑线上。
C++98 的主要语言特性是
- 模板——无约束的、图灵完备的、对泛型编程的编译期支持,在我早期工作([§2.1](#21-第一个十年))的
基础上进行了许多细化和改进;这项工作仍在继续([§6](06.md#6-概念))。
- 异常——一套在单独(不可见的)路径上返回错误值的机制,由调用方栈顶上的“在别处”的代码处理;见
([§7](07.md#7-错误处理))。
- `dynamic_cast` 和 `typeid`——一种非常简单的运行期反射形式(“运行期类型识别”,又名 RTTI)。
- `namespace`——允许程序员在编写由几个独立部分组成的较大程序时避免名称冲突。
- 条件语句内的声明——让写法更紧凑和限制变量作用域。
- 具名类型转换——(`static_cast`、`reinterpret_cast` 和 `const_cast`):消除了 C 风格的类
型转换中的二义性,并使显式类型转换更加显眼。
- `bool`:一种被证明非常有用和流行的布尔类型;C 和 C++ 曾经使用整数作为布尔变量和常量。
```cpp
void do_something(Shape* p)
{
if (Circle* pc = dynamic_cast<Circle*>(p)) { // p 是某种 Circle?
// ... 使用 pc 指向的 Circle ...
}
else {
// ... 不是 Circle,做其他事情 ...
}
}
```
一种更简单的变种是使用引用而不是指针:
```cpp
void do_something2(Shape& r)
{
Circle& rc = dynamic_cast<Circle&>(r); // r 是某种 Circle!
// ... 使用 rc 引用的 Circle ...
}
```
```cpp
void my_fct(const char* name) // C 风格的资源管理
{
FILE* p = fopen(name, "r"); // 打开文件 name 来读取
// ... 使用 p ...
fclose(p);
}
```
解决方案是将文件句柄表示为带有构造函数和析构函数的类:
```cpp
class File_handle {
FILE* p;
public:
File_handle(const char* name,const char* permissions); // 打开文件
~File_handle(); // 关闭文件
// ...
};
```
我们现在可以简化我们的用法:
```cpp
void my_fct2(const char* name) // RAII 风格的资源管理
{
File_handle p(name,"r"); // 打开文件 name 来读取
// ... 使用 p ...
} // p 被隐式地关闭
```
随着异常的引入,这样的资源句柄变得无处不在。特别的,标准库文件流就是这样一个资源句柄,所以使用 C+
+98 标准库,这个例子变成:
```cpp
void my_fct3(const string& name)
{
ifstream p(name); // 打开文件 name 来读取
// ... 使用 p ...
} // p 被隐式的关闭
```
请注意,RAII 代码不同于传统的函数使用,它允许在库中一劳永逸地定义“清理内存”,而不是程序员每次使
用资源时都必须记住并显式编写。至关重要的是,正确和健壮的代码更简单、更短,并且至少与传统风格一样高
效。在接下来的 20 年里,RAII 已遍布 C++ 库。
拥有非内存资源意味着垃圾收集本身不足以进行资源管理。此外,RAII 加上智能指针([§4.2.4]
(04.md#424-资源管理指针))消除了对垃圾收集的需求。另见([§10.6](10.md#106-编码指南))。
C++98 标准库提供了:
```cpp
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'
// ...
}
list<int>::iterator q
= find_if(lst.begin(), lst.end(), Greater_than<int>(42));
// ...
}
```
注意这里没有用到任何面向对象的方法。这是依赖模板的泛型编程,有时也被称为编译期多态。
```cpp
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()) {
// ...
}
// ...
}
```
- “低级编程”可以由少量的 C 或汇编代码处理。
- “高级编程”则可以使用一种带有巨大的运行时支持系统的更安全、更小并使用垃圾收集的语言来做,这样可以
更好、更便宜、更高效地完成。
- 像 Java 和 C# 这样的托管语言使用垃圾收集和一致的运行期范围检查,使得不太专业的程序员能更有生产
力,这样可以减少对高技能的开发人员的需求。
- 编程语言与平台的深度集成,并使用集成工具来进行支持,这对生产力和大型系统的构建至关重要。
显然,我和许多其他人并不同意。但这些在过去和现在都是严肃的争辩,它们如果正确的话应该导致 C++ 被放
弃使用。C++ 基于传统的编程语言模型,与底层操作系统分离,并由众多独立的工具供应者提供支持。托管语言
往往是专有的;只有一个庞大而富有的组织才能开发所需的庞大基础设施和库。我和 C++ 社区中的其他许多人
更喜欢不受公司控制的语言;这是我参加 ISO 标准工作的一个原因。
另一个转折点来自供应商,他们试图通过定义标准接口(比如图形用户界面)将自己喜欢的语言强加给所有用户,
而这只能通过使用他们喜欢的、通常是专有的语言来实现。比如谷歌对安卓系统使用 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%。
> 没有什么比开创一种新秩序更难于推行、更让人怀疑能否成功、处理起来更加危险。因为改革者会与所有从旧
秩序中获利的人为敌,而所有从新秩序中获利的人却只是冷淡的捍卫者。
## 2.4 其他语言
- `auto`——从初始化器推断类型的能力。它在现代语言中很流行,但也已由来已久。我不知它的最早起源,但
我在 1983 年实现这个功能的时候,也并不认为它很新颖([§4.2.1](04.md#421-auto-和-
decltype))。
- `tuple`——许多语言,特别是源自函数式编程传统的语言,都有元组,它通常是一个内置类型。C++ 标准库
`tuple` 及其许多用法都从中受到启发。`std::tuple` 派生自 `boost::tuple` [Boost 1998–2020]
([§4.3.4](04.md#434-tuple))。
- `regex`——加入 C++11 的标准库 `regex` 是(经由 Boost;已致谢)从 Unix 和 JavaScript 的功
能中拷贝来的([§4.6](04.md#46-c11 标准库组件))。
- 函数式编程——函数式编程特性和 C++ 构件之间有许多明显的相似之处。大多数不是简单的语言特性,而是
编程技巧。STL 受到函数式编程的启发,并首先在 Scheme [Stepanov 1986] 和 Ada [Musser and
Stepanov 1987] 中进行了尝试(未成功)。
- `future` 和 `promise`——源自 Multilisp,经由其他 Lisp 方言([§4.1.3](04.md#413-期值
future))。
- 范围 `for`——许多语言中都有对应物,但直接启发来自 STL 序列([§4.2.2](04.md#422-范围-
for))。
- `variant`、`any` 和 `optional`——显然受到多种语言的启发([§8.3](08.md#83-
variantoptional-和-any))。
- lambda 表达式——显然,部分灵感来自于函数式语言中 lambda 表达式的应用。但是,在 C++ 中,
lambda 表达式的根源还可以上溯到 BCPL 语言中用作表达式的代码块、局部函数(多次被 C 和 C++ 拒绝,
因其容易出错且增加了复杂性)和(最重要的)函数对象([§4.3.1](04.md#431-lambda-表达式))。
- `final` 和 `override`——用于更明确地管理类层次结构,并且在许多面向对象的语言中都可以使用。在早
期的 C++ 中已经考虑过它们了,但当时被认为是不必要的。
- 三向比较运算符 `<=>`,受 C 的 `strcmp` 及 PERL、PHP、Python 和 Ruby 语言的运算符的启发
([§9.3.4](09.md#934-))。
- `await`——C++ 里最早的协程([§1.1](01.md#11-年表))受 Simula 启发,但是作为库提供,而不是
作为语言特性,这是为了给其他替代的并发技术留出空间。C++20 中的无栈协程的思想主要来自
F#([§9.3.2](09.md#932-协程))。
同样,C++ 对其他语言的贡献也难以估量。通常,类似的特性是平行演化的,或有着共同的根源。例如: