第19章 历史与兼容性
第19章 历史与兼容性
- 历史
- 时间线
- 早期岁月
- ISO C++标准
- 标准与编程风格
- C++的使用
- C++模型
- C++特性演变
- C++11语言特性
- C++14语言特性
- C++17语言特性
- C++20语言特性
- C++11标准库组件
- C++14标准库 组件
- C++17标准库组件
- C++20标准库 组件
- 移除和弃用的特性
- C/C++兼容性
- C和C++是兄弟语言
- 兼容性问题
- 参考文献
- 建议
19.1 历史
我发明了C++,编写了它的早期定义,并制作了它的第一个实现。我选择了C++的设计标准并对其进行了规范,设计了它的主要语言特性,开发或帮助开发了许多早期的库,并且在25年内负责处理C++标准委员会中的扩展提案。
C++旨在提供Simula的程序组织设施[Dahl,1970],以及C在系统编程方面的效率和灵活性[Kernighan,1978]。Simula是C++抽象机制的最初来源。类的概念(包括派生类和虚函数)就是从Simula借鉴而来的。然而,模板和异常后来才加入到C++中,它们的灵感来源于不同的地方。
C++的发展始终是在其使用的背景下进行的。我花了很多时间倾听用户的声音,寻求经验丰富的程序员的意见,当然也在编写代码。特别是,在我任职AT&T贝尔实验室期间,我的同事们对C++在其首个十年间的发展至关重要。
本节是一个简要的概述,它并未尝试提及每一种语言特性和库组件。此外,它也没有深入探讨细节。如需更多信息,特别是更多做出贡献的人员名单,请参阅我在ACM编程语言历史会议上的三篇论文[Stroustrup,1993][Stroustrup,2007][Stroustrup,2020]以及我的《C++的设计与演化》一书(简称“D&E”)[Stroustrup,1994]。它们详细描述了C++的设计和演化过程,并记录了C++对其他编程语言的影响以及其他编程语言对C++的影响。我试图在标准设施和提出并完善这些设施的人员之间保持联系。C++并非一个面无表情、匿名的委员会或所谓“终身独裁者”的作品;它是许多敬业、经验丰富、勤奋工作的个人的成果。
作为ISO C++标准工作的一部分而产生的大多数文件都可以在网上找到[WG21]。
19.1.1 时间线
导致C++诞生的工作始于1979年秋季,当时它被称为“带类的C”。以下是一个简化的时间线:
1979年 “带类的C”工作开始。最初的功能集包括类和派生类、公有/私有访问控制、构造函数和析构函数,以及带参数检查的函数声明。第一个库支持非抢占式并发任务和随机数生成器。
1984年 “带类的C”被重命名为C++。到那时,C++已经获得了虚函数、函数和运算符重载、引用,以及I/O流和复数库。
1985年 C++首次商业发布(10月14日)。该库包括I/O流、复数和任务(非抢占式调度)。
1985年 《C++编程语言》(“TC++PL”,10月14日)[Stroustrup,1986]。
1989年 《C++注解参考手册》(“ARM”)[Ellis,1989]。
1991年 《C++编程语言》第二版[Stroustrup,1991],介绍了使用模板的泛型编程和基于异常的错误处理,包括“资源获取即初始化”(RAII)这一通用资源管理习惯用法。
1997年 《C++编程语言》第三版[Stroustrup,1997]引入了ISO C++,包括命名空间、 dynamic_cast 和许多模板的改进。标准库增加了STL框架的泛型容器和算法。
1998年 ISO C++标准[C++,1998]。
2002年 开始修订标准的工作,俗称C++0x。
2003年 发布了ISO C++标准的“错误修正”修订版[C++,2003]
2011年 ISO C++11标准[C++,2011]提供统一初始化、移动语义、从初始化器推导类型( auto )、范围 for 循环、可变参数模板、lambda表达式、类型别名、适合并发的内存模型等。标准库增加了 线程 、锁、正则表达式、哈希表( unordered_map )、资源管理指针( unique_ptr 和 shared_ptr )等。
2013年 出现了第一个完整的C++11实现。
2013年 《C++编程语言》第四版介绍了C++11。
2014年 ISO C++14标准[C++,2014]完善了C++11,增加了变量模板、数字分隔符、泛型lambda表达式和一些标准库的改进。完成了第一个C++14实现。
2015年 C++核心准则项目启动[Stroustrup,2015]。
2017年 ISO C++17标准[C++,2017]提供了一组多样化的新功能,包括求值顺序保证、结构化绑定、折叠表达式、文件系统库、并行算法以及 variant 和 optional 类型。完成了第一个C++17实现。
2020年 ISO C++20标准[C++,2020]提供了 模块 、 概念 、协程、范围、printf()风格格式化、日历和许多小功能。完成了第一个C++20实现。
在开发过程中,C++11曾被称为C++0x。正如大型项目中常见的情况一样,我们对完成日期过于乐观。接近尾声时,我们开玩笑说C++0x中的’x’是十六进制,所以C++0x变成了C++0B。另一方面,委员会以及主要的编译器提供商都按时发布了C++14、C++17和C++20。
19.1.2 早期岁月
我最初设计和实现这种语言是因为我希望在多处理器和局域网(现在被称为多核和集群)上分发UNIX内核的服务。为此,我需要精确指定系统的各个部分以及它们如何通信。Simula [Dahl,1970]本将是理想的选择,但出于性能考虑,它并不适合。我还需要直接处理硬件并提供高性能的并发编程机制,在这方面C语言会是理想的选择,但它在模块化和类型检查方面的支持较弱。因此,我将Simula风格的类添加到C语言(经典C;§19.3.1)中,得到的“带类的C”被用于一些重大项目,在这些项目中,其编写时间和空间占用极小的程序的能力得到了严格测试。但它缺乏运算符重载、引用、虚函数、模板、异常处理以及许多其他细节[Stroustrup,1982]。C++首次在研究机构之外的使用始于1983年7月。
“C++”(发音为“see plus plus”)这个名字是由Rick Mascitti在1983年夏天创造的,并由我选择作为“带类的C”的替代名称。这个名字象征着从C语言演变而来的性质;“++”是C语言的递增运算符。稍微短一些的名字“C+”是一个语法错误,而且它已经被用作另一种不相关语言的名称。熟悉C语言语义的人认为C++不如++C优雅。这种语言没有被命名为D,因为它是C语言的扩展,因为它没有试图通过移除特性来解决问题,而且当时已经存在几种被称为D的C语言后继者。关于C++名称的另一种解释,请参见[Orwell,1949]的附录。
C++的设计初衷主要是为了让我和我的朋友们不必再使用汇编语言、C语言或当时流行的各种高级语言进行编程。它的主要目的是让个体程序员更容易、更愉快地编写优秀的程序。在早期,没有C++的书面设计规范;设计、文档和实现是同时进行的。那时既没有“C++项目”,也没有“C++设计委员会”。自始至终,C++都在不断发展,以解决用户遇到的问题,以及我和我的朋友、同事们讨论的结果。
C++的最初设计包括带参数类型检查和隐式转换的函数声明、具有接口和实现之间 公有/私有 区别的类、派生类、构造函数和析构函数。我使用宏来提供原始的参数化[Stroustrup,1982]。到1980年年中,它已经在非实验性环境中使用。那年年底,我能够提出一套支持一系列连贯编程风格的语言设施。回想起来,我认为引入构造函数和析构函数是最重要的。用当时的术语来说[Stroustrup,1979]:
“new function”为成员函数创建执行环境;“delete function”则逆转这一过程。
不久之后,“new function”和“delete function”被重命名为“构造函数”和“析构函数”。这是C++资源管理策略(导致对异常的需求)的根源,也是使用户代码简短明了的关键技术之一。如果当时有其他语言支持能够执行通用代码的多个构造函数,我不知道(也不了解)这些语言。析构函数是C++中的新特性。
C++于1985年10月商业化发布。到那时,我已经添加了内联(§ 1.3,§ 5.2.1)、 const (§ 1.6)、函数重载(§ 1.3)、引用(§ 1.7)、运算符重载(§ 5.2.1,§ 6.4)和虚函数(§ 5.4)。在这些特性中,以虚函数形式提供的运行时多态性支持无疑是最具争议的。我从Simula中了解到了它的价值,但发现很难说服大多数系统编程领域的人相信它的价值。系统程序员往往对间接函数调用持怀疑态度,而熟悉其他支持面向对象编程语言的程序员则很难相信虚函数在系统代码中足够快以至于有用。相反,许多具有面向对象背景的程序员(现在仍然有很多)很难适应这样的想法:你仅使用虚函数调用来表达必须在运行时做出的选择。对虚函数的抵制可能与对通过编程语言支持的更规整的代码结构来获得更好的系统的想法的抵制有关。许多C程序员似乎坚信真正重要的是完全的灵活性和对程序每个细节的仔细设计。我的观点是(并且仍然是)我们需要从语言和工具中获得尽可能多的帮助:我们正在尝试构建的系统的固有复杂性总是处于我们所能表达的边缘。
早期的文档(例如,[Stroustrup,1985]和[Stroustrup,1994])这样描述C++:
C++是一种通用编程语言,它
- 是更好的C
- 支持数据抽象
- 支持面向对象编程
请注意,我并没有说“C++是一种面向对象编程语言”。这里的“支持数据抽象”指的是信息隐藏、不属于类层次的类以及泛型编程。最初,泛型编程通过宏的使用得到了很差的支持[Stroustrup,1982]。模板和概念要晚得多才出现。C++的许多设计工作都是在我的同事们的黑板上完成的。在早期,Stu Feldman、Alexander Fraser、Steve Johnson、Brian Kernighan、Doug McIlroy和Dennis Ritchie的反馈是无价的。
在20世纪80年代的后半段,我继续根据用户评论和我的C++一般目标添加语言特性。其中最重要的是模板[Stroustrup,1988]和异常处理[Koenig,1990],在标准化工作开始时,它们被认为是实验性的。在设计模板时,我不得不在灵活性、效率和早期类型检查之间做出选择。当时,没有人知道如何同时获得这三者。为了与用于要求苛刻的系统应用程序的C风格代码竞争,我认为我必须选择前两个属性。回想起来,我认为这个选择是正确的,而对模板进行更好的类型检查的持续探索[DosReis,2006][Gregor,2006][Sutton,2011][Stroustrup,2012a][Stroustrup,2017]导致了C++20中的概念(第8章)。异常处理的设计重点是多级异常传播、将任意信息传递给错误处理程序,以及通过使用具有析构函数的局部对象来表示和释放资源,将异常和资源管理集成在一起。我笨拙地将这种关键技术命名为“资源获取即初始化”(Resource Acquisition Is Initialization),其他人很快将其简化为缩写RAII(§ 6.3)。
我将C++的继承机制推广到支持多个基类[Stroustrup,1987]。这被称为多重继承,并被认为是困难且有争议的。我认为它远不如模板或异常重要。抽象类的多重继承(通常称为接口)现在在支持静态类型检查和面向对象编程的语言中非常普遍。
C++语言与一些关键的库设施一起发展。例如,我设计了复数[Stroustrup,1984]、向量、栈和(I/O)流类[Stroustrup,1985],并与运算符重载机制一起设计。第一个字符串和列表类是由Jonathan Shopiro和我一起开发的,作为同一努力的一部分。Jonathan的字符串和列表类是作为库的一部分首次广泛使用的。标准C++库中的字符串类起源于这些早期努力。在[Stroustrup,1987b]中描述的任务库是1980年编写的第一个“带类的C”程序的一部分。它提供了协程和调度器。我编写它和它的相关类来支持Simula风格的模拟。它对C++在20世纪80年代的成功和广泛应用至关重要。不幸的是,我们不得不等到2011年(30年!)才得到标准化的并发支持,并使其普遍可用(§ 18.6)。协程是C++20的一部分(§ 18.6)。模板设施的发展受到了Andrew Koenig、Alex Stepanov、我以及其他人设计的各种 vector , map , list , 和 sort 模板的影响。
1998年标准库中最重要的创新是STL,一个算法和容器的框架(第12章,第13章)。这是Alex Stepanov(与Dave Musser、Meng Lee等人)的工作成果,基于十多年的泛型编程工作。STL在C++社区内外都产生了巨大的影响。
C++是在一个拥有众多已确立和实验性编程语言的环境中成长起来的(例如,Ada [Ichbiah,1979]、Algol 68 [Woodward,1974]和ML [Paulson,1996])。当时,我熟悉大约25种语言,它们对C++的影响记录在[Stroustrup,1994]和[Stroustrup,2007]中。然而,决定性的影响总是来自于我遇到的应用程序。我故意让C++的演变“以问题为导向”,而不是模仿性的。
19.1.3 ISO C++标准
C++使用的爆炸性增长带来了一些变化。大约在1987年的某个时候,很明显C++的正式标准化是不可避免的,而且我们需要为标准化工作奠定基础[Stroustrup,1994]。结果是,我们有意识地努力保持C++编译器实现者与其主要用户之间的联系。这是通过纸质和电子邮件,以及在C++会议和其他地方的面对面会议来实现的。
AT&T贝尔实验室通过允许我与实现者和用户分享修订后的C++参考手册草案,对C++及其更广泛的社区做出了重大贡献。由于这些人中的许多人在与AT&T竞争的公司工作,因此不应低估这一贡献的重要性。一个不那么开明的公司仅仅通过无所作为就可能导致语言碎片化的重大问题。事实上,来自几十个组织的约一百人阅读并评论了最终成为普遍接受的参考手册和ANSI C++标准化工作基础文档的内容。他们的名字可以在《 C++注解参考手册 》(“ARM”)[Ellis,1989]中找到。ANSI的X3J16委员会于1989年12月在惠普、DEC和IBM的倡议下,并在AT&T的支持下召开。1991年6月,这项ANSI(美国国家标准)C++标准化工作成为ISO(国际标准)C++标准化工作的一部分。ISO C++委员会被称为WG21。从1990年开始,这些联合C++标准委员会一直是C++发展和定义完善的主要论坛。我始终服务于这些委员会。特别是,从1990年到2014年,我担任扩展工作组(后来称为进化组)的主席,直接负责处理对C++进行重大更改的提案以及添加新语言特性的工作。1995年4月,发布了供公众审查的初步标准草案。第一个ISO C++标准(ISO/IEC 14882-1998)[C++,1998]于1998年以22票赞成、0票反对的全国投票获得批准。2003年发布了该标准的“错误修正版”,因此您有时会听到人们提到C++03,但实际上它与C++98是同一种语言和标准库。
C++11,多年来被称为C++0x,是WG21成员的工作成果。委员会在日益繁重的自我施加的流程和程序下工作。这些流程可能导致了更好的(更严格的)规范,但也限制了创新[Stroustrup,2007]。2009年发布了供公众审查的初步标准草案。第二个ISO C++标准(ISO/IEC 14882-2011)[C++,2011]于2011年8月以21票赞成、0票反对的全国投票获得批准。两个标准之间间隔时间较长的一个原因是,委员会的大多数成员(包括我在内)都错误地认为ISO规则要求在发布标准后有一个“等待期”,之后才能开始新特性的工作。因此,直到2002年才开始认真研究新的语言特性。其他原因包括现代语言及其基础库规模的增加。就标准文本页数而言,语言增长了约30%,标准库增长了约100%。增长的大部分原因是更详细的规范,而不是新功能。此外,显然必须非常谨慎地制定新的C++标准,以免通过不兼容的更改破坏旧代码。委员会必须不破坏数十亿行的C++代码。几十年的稳定性是一个重要的“特性”。
C++11极大地丰富了标准库,并致力于完善C++98中已被证明成功的“范式”和习惯用法的编程风格所需的功能集。
C++11工作的总体目标是:
- 使C++成为更好的系统编程和库构建语言。
- 使C++更容易教授和学习。
这些目标在[Stroustrup,2007]中有详细记载。
我们做出了巨大努力,使并发系统编程类型安全且可移植。这涉及内存模型(§ 18.1)和对无锁编程的支持,这是并发工作组中Hans Boehm、Brian McKnight等人的工作成果。在此基础上,我们添加了 线程 库。
C++11之后,人们普遍认为标准之间的13年时间太长了。Herb Sutter提议委员会采用固定间隔准时发布的“火车模型”。我强烈主张标准之间的间隔要短,以最大限度地减少因某人坚持要额外时间以允许包含“仅仅多一个必要特性”而导致的延误。我们同意了一个雄心勃勃的三年计划,想法是我们应该在次要版本和主要版本之间交替发布。
C++14故意被定义为一个次要版本,旨在“完成C++11”。这反映了这样一个现实:在固定发布日期的情况下,我们知道我们想要哪些特性,但可能无法按时交付。此外,一旦广泛使用,特性集中的差距就不可避免地会被发现。
C++17旨在成为一个主要版本。这里的“主要”是指包含将改变我们对软件结构思考方式以及我们设计软件方式的特性的版本。根据这个定义,C++17充其量是一个中等版本。它包含了许多次要扩展,但那些将带来巨大变化的特性(例如,概念、模块和协程)要么尚未准备好,要么陷入了争议和缺乏设计方向的困境。因此,C++17包含了对每个人都有一点用处的东西,但没有什么能显著改变已经吸收了C++11和C++14教训的C++程序员的生活。
C++20提供了长期承诺且急需的主要特性,如模块(§ 3.2.2)、概念(§ 8.2)、协程(§ 18.6)、范围(§ 14.5)以及许多次要特性。它是对C++的一次重大升级,就像C++11一样。它在2021年底得到了广泛应用。
ISO C++标准委员会SC22/WG21现在大约有350名成员,其中大约250名成员参加了最后一次疫情前的面对面会议,该会议在布拉格举行,C++20以79票赞成、0票反对的一致意见获得批准,随后以22票赞成、0票反对的全国投票获得批准。在如此庞大且多样化的群体中达成这样的共识是一项艰巨的工作。危险包括“委员会设计”、功能膨胀、缺乏一致的风格和短视的决策。朝着更易于使用和更连贯的语言迈进是非常困难的。委员会意识到了这一点并正在努力克服;请参阅[Wong,2020]。有时,我们成功了,但很难避免“次要有用特性”、时尚和专家直接服务罕见特殊情况的愿望带来的复杂性悄悄增加。
19.1.4 标准与风格
标准说明了什么可行以及如何可行,但它并没有说明什么构成良好和有效的使用。理解编程语言特性的技术细节与有效地结合其他特性、库和工具来生产更好的软件之间存在显著差异。这里的“更好”指的是“更易于维护、更少出错、更快”。我们需要开发、推广和支持连贯的编程风格。此外,我们还必须支持旧代码向这些更现代、更有效、更连贯的风格演变。
随着语言和标准库的不断发展,推广有效的编程风格变得至关重要。让一大群程序员放弃那些能工作的东西而转向更好的东西是极其困难的。仍然有人将C++视为C语言的几个微小补充,也有人认为基于庞大类层次结构的20世纪80年代的面向对象编程风格是发展的巅峰。许多人在拥有大量旧C++代码的环境中仍在努力良好地使用现代C++。另一方面,也有许多人过度热衷于使用新特性。例如,一些程序员坚信只有使用大量模板元编程的代码才是真正的C++。
什么是现代C++?2015年,我着手回答这个问题,通过制定一套有充分理由支持的编码指南。很快我就发现,我并不是唯一一个在努力解决这个问题的人。与世界各地的许多人,特别是来自微软、红帽和脸书的人一起,我们启动了“C++核心指南”项目[Stroustrup,2015]。这是一个雄心勃勃的项目,旨在以完全的类型安全和资源安全为基础,实现更简单、更快、更安全、更易于维护的代码[Stroustrup,2015b][Stroustrup,2021]。除了具有充分理由的具体编码规则外,我们还通过静态分析工具和一个小型支持库来支持这些指南。我认为,类似这样的东西对于推动整个C++社区向前发展,从语言特性、库和支持工具的改进中受益是必不可少的。
19.1.5 C++的使用
C++现在是一种非常广泛使用的编程语言。其用户群体从1979年的一个人迅速增长到1991年的约40万人;也就是说,用户数量在十多年的时间里大约每7.5个月翻一番。当然,自那次初期的快速增长之后,增长率有所放缓,但据我最好的估计,2018年约有450万C++程序员[Kazakova,2015],而今天(2022年)可能又增加了一百万。大部分增长发生在2005年之后,当时处理器速度呈指数级增长的趋势停止,因此语言性能变得越来越重要。这种增长是在没有正式营销或组织化用户社区的情况下实现的[Stroustrup,2020]。
C++主要是一种工业语言,即在工业界的应用比在教育界或编程语言研究领域更为突出。它在贝尔实验室诞生,受电信和系统编程(包括设备驱动程序、网络和嵌入式系统)各种严格需求的启发而发展。从那时起,C++的使用几乎扩展到了所有行业:微电子、Web应用程序和基础设施、操作系统、金融、医疗、汽车、航空航天、高能物理、生物学、能源生产、机器学习、视频游戏、图形、动画、虚拟现实等等。它主要用于那些需要C++有效使用硬件和管理复杂性的能力的问题。这似乎是一个不断扩展的应用领域[Stroustrup,1993][Stroustrup,2014][Stroustrup,2020]。
19.1.6 C++模型
C++语言可以概括为一组相互支持的特性:
- 一个静态类型系统,对内置类型和用户定义类型提供同等支持(第1章、第5章、第6章)
- 值和引用语义(§ 1.7、§ 5.2、§ 6.2、第12章、§ 15.2)
- 系统且通用的资源管理(RAII)(§ 6.3)
- 支持高效的对象导向编程(§ 5.3、class.virtual、§ 5.5)
- 支持灵活且高效的泛型编程(第7章、第18章)
- 支持编译时编程(§ 1.6、第7章、第8章)
- 直接使用机器和操作系统资源(§ 1.4、第18章)
- 通过库(通常使用内建函数实现)提供并发支持(第18章)
标准库组件为这些高级目标提供了进一步的必要支持。
19.2 C++特性演变
在这里,我列出了为C++11、C++14、C++17和C++20标准添加到C++的语言特性和标准库组件。
19.2.1 C++11语言特性
查看语言特性列表可能会令人困惑。请记住,语言特性并不是孤立使用的。特别是,C++11中的大多数新特性如果脱离由旧特性提供的框架,就毫无意义。
- 使用 {} 列表的统一和通用初始化(§ 1.4.2、§ 5.2.3)
- 从初始化器推导类型: auto (§ 1.4.2)
- 防止缩窄(§ 1.4.2)
- 广义和保证常量表达式: constexpr (§ 1.6)
- 范围 for 语句(§ 1.7)
- 空指针关键字: nullptr (§ 1.7.1)
- 作用域枚举和强类型枚举: enum class (§ 2.4)
- 编译时断言: static_assert (§ 4.5.2)
- {} 列表到 std::initializer_list 的语言映射(§ 5.2.3)
- 右值引用,支持移动语义(§ 6.2.2)
- Lambda表达式(§ 7.3.3)
- 可变参数模板(§ 7.4.1)
- 类型和模板别名(§ 7.4.2)
- Unicode字符
- long long 整数类型
- 对齐控制: alignas 和 alignof
- 在声明中使用表达式的类型作为类型: decltype
- 原始字符串字面量(§ 10.4)
- 后缀返回类型语法(§ 3.4.4)
- 属性语法和两个标准属性:[[ carries_dependency ]]和[[ noreturn ]]
- 防止异常传播的方式: noexcept 说明符(§ 4.4)
- 测试表达式中抛出异常的可能性: noexcept 运算符
- C99特性:扩展的整数类型(即可选的更长整数类型的规则);窄/宽字符串的连接; __STDC_HOSTED__ ; _Pragma(X) ;可变参数宏和空宏参数
- __func__ 作为持有当前函数名称的字符串名称
- inline 命名空间
- 委托构造函数
- 类内成员初始化(§ 6.1.3)
- 控制默认行为: default 和 delete (§ 6.1.1)
- 显式转换运算符
- 用户定义字面量(§ 6.6)
- 对模板实例化的更明确控制: extern 模板
- 函数模板的默认模板参数
- 继承构造函数(§ 12.2.2)
- 重写控制: override (§ 5.5)和 final
- 更简单且更通用的SFINAE(替换失败不是错误)规则
- 内存模型(§ 18.1)
- 线程局部存储: thread_local
有关C++11中对C++98所做更改的更完整描述,请参阅[Stroustrup,2013]。
19.2.2 C++14 语言特性
- 函数返回类型推导;(§3.4.3)
- 改进的 constexpr 函数,例如,允许使用 for 循环;(§1.6)
- 变量模板;(§7.4.1)
- 二进制字面量;(§1.4)
- 数字分隔符;(§1.4)
- 泛型 lambda 表达式;(§7.3.3.1)
- 更通用的 lambda 捕获;
- [[deprecated]] 属性;
- 其他一些较小的扩展。
19.2.3 C++17 语言特性
- 保证的拷贝省略(§6.2.2)
- 对过度对齐类型的动态分配
- 更严格的求值顺序(§1.4.1)
- UTF-8 字面量( u8 )
- 十六进制浮点数字面量(§11.6.1)
- 折叠表达式(§8.4.1)
- 泛型值模板参数(自动模板参数;§8.2.5)
- 类模板参数类型推导(§7.2.3)
- 编译时 if 语句(§7.4.3)
- 带初始化的选择语句(§1.8)
- constexpr lambda表达式
- 内联变量
- 结构化绑定(§3.4.5)
- 新的标准属性: [[fallthrough]] 、 [[nodiscard]] 和 [[maybe_unused]]
- std::byte 类型(§16.7)
- 通过其底层类型的值初始化 枚举 (§2.4)
- 其他一些较小的扩展
19.2.4 C++20 语言特性
- 模块(§3.2.2)
- 概念(§8.2)
- 协程(§18.6)
- 指定初始化器(C99 特性的稍作限制版本)
- <=> (“太空船操作符”),一种三方比较(§6.5.1)
- [∗this] 用于按值捕获当前对象(§7.3.3)
- 标准属性 [[no_unique_address]] 、 [[likely]] 和 [[unlikely]]
- constexpr 函数中允许使用更多设施,包括 new 、 union 、 dynamic_cast 和 typeid
- consteval 函数,保证编译时求值(§1.6)
- constinit 变量,保证静态(非运行时)初始化(§1.6)
- 使用作用域枚举(§2.4)
- 其他一些较小的扩展
19.2.5 C++11 标准库组件
C++11 对标准库的扩展包括两种形式:新组件(如正则表达式匹配库)和对 C++98 组件的改进(如容器的移动构造函数)。
- 容器的 initializer_list 构造函数(§5.2.3)
- 容器的移动语义(§6.2.2,§13.2)
- 单链表: forward_list (§12.3)
- 哈希容器: unordered_map 、 unordered_multimap 、 unordered_set 和 unordered_multiset (§12.6,§12.8)
- 资源管理指针: unique_ptr 、 shared_ptr 和 weak_ptr (§15.2.1)
- 并发支持: thread (§18.2)、互斥锁和锁(§18.3)以及条件变量(§18.4)
- 高级并发支持: packaged_thread 、 future 、 promise 和 async() (§18.5)
- tuple (§15.3.4)
- 正则表达式: regex (§10.4)
- 随机数:分布和引擎(§17.5)
- 整数类型名称,如 int16_t 、 uint32_t 和 int_fast64_t (§17.8)
- 固定大小的连续序列容器: array (§15.3)
- 复制和重新抛出异常(§18.5.1)
- 使用错误代码进行错误报告: system_error
- 容器的 emplace() 操作(§12.8)
- 广泛使用 constexpr 函数
- 系统性使用 noexcept 函数
- 改进的函数适配器: function 和 bind() (§16.3)
- 字符串到数值的转换
- 作用域分配器
- 类型特性,如 is_integral 和 is_base_of (§16.4.1)
- 时间工具: duration 和 time_point (§16.2.1)
- 编译时有理数算术: ratio
- 放弃进程: quick_exit (§16.8)
- 更多算法,如 move() 、 copy_if() 和 is_sorted() (第 13 章)
- 垃圾收集 API;后来已弃用(§19.2.9)
- 低级并发支持: 原子 操作(§18.3.2)
- 其他一些较小的扩展
19.2.6 C++14 标准库组件
- shared_mutex 和 shared_lock (§18.3)
- 用户定义字面量(§6.6)
- 按类型访问 tuple (§15.3.4)
- 关联容器异构查找
- 其他一些较小的扩展
19.2.7 C++17 标准库组件
- 文件系统(§11.9)
- 并行算法(§13.6,§17.3.1)
- 数学特殊函数(§17.2)
- string_view (§10.3)
- any (§15.4.3)
- variant (§15.4.1)
- optional (§15.4.2)
- 一种用于调用给定一组参数时可以调用的任何内容的方法:基本字符串转换: to_chars() 和 from_chars()
- 多态分配器(§12.7)
- scoped_lock (§18.3)
- 其他一些较小的扩展
19.2.8 C++20 标准库组件
- 范围、视图和管道(§14.1)
- printf() 风格格式化: format() 和 vformat() (§11.6.2)
- 日历(§16.2.2)和时区(§16.2.3)
- span ,用于对连续数组的读写访问(§15.2.2)
- source_location (§16.5)
- 数学常数,例如 pi 和 ln10e (§17.9)
- 对 原子 操作的许多扩展(§18.3.2)
- 等待多个线程的方法: barrier 和 latch
- 功能测试宏
- bit_cast<> (§16.7)
- 位操作(§16.7)
- 更多标准库函数被标记为 constexpr
- 在标准库中广泛使用 <=>
- 许多其他较小的扩展
19.2.9 移除和弃用的特性
在现存的数十亿行C++代码中,没有人确切知道哪些特性处于关键使用状态。因此,ISO委员会只有在多年警告之后才勉强移除旧特性。然而,有时一些麻烦的特性会被移除或弃用。
通过弃用一个特性,标准委员会表达了希望该特性消失的愿望。然而,委员会没有权力立即移除一个被广泛使用的特性,无论它多么冗余或危险。因此,弃用是一个强烈的暗示,表明应避免使用该特性。它可能在未来消失。弃用特性的列表位于标准的附录D中[C++,2020]。编译器可能会对使用弃用特性的代码发出警告。然而,弃用的特性仍然是标准的一部分,并且由于兼容性的原因,历史表明它们往往会被“永远”支持。即使最终被移除的特性,由于用户对实现者的压力,也往往会在实现中继续存在。
- 移除:异常规范:void f() throw(X,Y); // C++98; 现在是错误
- 移除:支持异常规范的设施,unexpected_handler, set_unexpected(), get_unexpected(), 和 unexpected()。请改用noexcept(§4.2)。
- 移除:三字符组。
- 移除:auto_ptr。请改用unique_ptr(§15.2.1)。
- 移除:存储说明符register的使用。
- 移除:对bool使用++。
- 移除:C++98的export特性。它很复杂,且主要供应商未提供支持。相反,export现在用作模块的关键字(§3.2.2)。
- 弃用:为具有析构函数的类生成复制操作(§6.2.1)。
- 移除:将字符串字面量赋值给char*。请改用const char*或auto。
- 移除:一些C++标准库函数对象和相关函数。大多数与参数绑定有关。请改用lambda和function(§16.3)。
- 弃用:将枚举值与来自不同枚举或浮点值的值进行比较。
- 弃用:比较两个数组。
- 弃用:下标中的逗号操作(例如,[a,b])。为了允许用户定义的operator具有多个参数。
- 弃用:lambda表达式中隐式捕获*this。请改用[=,this](§7.3.3)。
- 移除:垃圾收集器的标准库接口。C++垃圾收集器不使用该接口。
- 弃用:strstream;请改用spanstream(§11.7.4)。
19.3 C/C++ 兼容性
除了少数例外,C++ 是 C(指 C11;[C,2011])的一个超集。大多数差异源于 C++ 对类型检查的更多强调。编写良好的 C 程序往往也是 C++ 程序。例如,K&R2 [Kernighan,1988] 中的每个示例都是 C++ 程序。编译器可以诊断 C++ 和 C 之间的所有差异。C11/C++20 的不兼容之处列在标准的附录 C 中 [C++,2020]。
19.3.1 C 和 C++ 是“兄弟姐妹”
为什么我可以称 C 和 C++ 为“兄弟姐妹”呢?让我们看一个简化的“家族树”:
经典C语言有两个主要的后裔:ISO C和ISO C++。多年来,这两种语言以不同的速度和方向发展。其结果之一是,每种语言都以略微不同的方式支持传统的C语言风格编程。由此产生的不兼容性可能会给同时使用C和C++的人、使用另一种语言编写的库编写代码的人,以及为C和C++编写库和工具的人带来困扰。
实线表示大量特性的继承,虚线表示主要特性的借用,点线表示次要特性的借用。由此,ISO C和ISO C++作为K&R C [Kernighan,1978]的两个主要后裔,并作为兄弟语言出现。它们都保留了经典C语言的关键方面,但都与经典C语言不完全兼容。我从一张曾经贴在Dennis Ritchie终端上的贴纸上选出了“经典C”这个词。它是K&R C加上枚举和 结构 赋值。BCPL由[Richards,1980]定义,C89由[C1990]定义。
曾经有一个C++03版本,但我没有列出,因为它是一个错误修复版本。同样,C17也没有列出,因为它是C11的错误修复版本。
请注意,C和C++之间的差异并不一定是C++中对C所做的更改的结果。在许多情况下,不兼容是由C语言在很久之后才采用的不兼容特性引起的,而这些特性在C++中已经很常见了。例如,将 T* 赋值给 void* 的能力以及全局 const 的链接[Stroustrup,2002]。有时,甚至在成为ISO C++标准的一部分之后,某个特性也被不兼容地采用到C语言中,比如 inline 含义的细节。
19.3.2 兼容性问题
C和C++之间存在许多细微的不兼容性。所有这些都可能给程序员带来问题,但在C++的上下文中,所有这些问题都可以得到处理。如果没有其他办法,C代码片段可以作为C代码编译,并使用 extern "C" 机制进行链接。
将C程序转换为C++时,主要可能遇到的问题包括:
• 设计不佳和编程风格不佳。
• void* 被隐式转换为 T* (即,在没有进行类型转换的情况下进行转换)。
• C++中的关键字,如 class 和 private ,在C代码中被用作标识符。
• 作为C代码编译的代码片段和作为C++代码编译的代码片段之间的链接不兼容。
19.3.2.1 风格问题
当然,C程序是用C语言风格编写的,比如K&R [Kernighan,1988]中使用的风格。这意味着广泛使用指针和数组,以及可能的大量宏。在一个大型程序中,这些设施很难可靠地使用。资源管理和错误处理往往是临时的(而不是由语言和工具支持的),并且往往文档记录不完整,遵守也不严格。将C程序逐行简单地转换为C++程序,得到的程序往往只是稍微好一点,检查得更仔细一些。事实上,我从来没有在将C程序转换为C++程序时没有发现一些错误。然而,基本结构没有改变,错误的根本来源也没有改变。如果你在原始的C程序中存在不完整的错误处理、资源泄漏或缓冲区溢出,它们在C++版本中仍然会存在。为了获得主要的好处,你必须对代码的基本结构做出改变:
不要把C++看作是只添加了几个特性的C语言。C++可以这样使用,但只是次优的选择。与C相比,要从C++中获得真正的主要优势,你需要应用不同的设计和实现风格。[2][3][4][5][6][7][8][9][10][11][12][13](这些编号可能是为了列出更多的建议或步骤,但在这里没有给出具体的内容,所以我会继续解释当前段落的内容。)
把C++标准库当作学习新技术和编程风格的老师。注意它与C标准库的不同(例如,使用 = 而不是 strcpy() 进行复制)。
在C++中,几乎永远不需要宏替换。使用 const (§1.6)、 constexpr (§1.6)、 enum 或 enum class (§2.4)来定义常量,使用 constexpr (§1.6)、 consteval (§1.6)和 inline (§5.2.1)来避免函数调用开销,使用模板(第7章)来指定函数和类型的族,以及使用命名空间(§3.3)来避免名称冲突。
在需要变量之前不要声明它,并立即初始化它。声明可以在任何可以出现语句的地方出现(§1.8),例如在 for 语句的初始化器和条件中(§1.8)。
不要使用 malloc() 。 new 运算符(§5.2.2)可以更好地完成同样的工作,而代替 realloc() ,可以尝试使用 vector (§6.3,§12.2)。不要仅仅用“裸”的 new 和 delete (§5.2.2)来替换 malloc() 和 free() 。
避免使用 void*、 union 和类型转换,除了在某些函数或类的实现深处。它们的使用限制了你可以从类型系统中获得的支持,并可能损害性能。在大多数情况下,类型转换是设计错误的迹象。
如果你必须使用显式类型转换,请使用适当的命名转换(例如, static_cast ;§5.2.3),以更精确地说明你试图做什么。
尽量减少数组和C风格字符串的使用。C++标准库的字符串(§10.2)、数组(§15.3.1)和向量(§12.2)通常可以用来编写更简单、更易维护的代码,相比传统的C风格。一般来说,尽量不要自己构建标准库已经提供的东西。
避免指针算术,除非在非常专门的代码中(如内存管理器)。
将连续的序列(如数组)作为 span (§15.2.2)传递。这是一种避免范围错误(“缓冲区溢出”)的好方法,无需额外的测试。
对于简单的数组遍历,使用范围 for 循环(§1.7)。它比传统的C循环更容易编写,速度一样快,而且更安全。
使用 nullptr (§1.7.1)而不是 0 或 NULL 。
不要假设用C风格费力编写的东西(避免C++特性,如类、模板和异常)比更短的替代方案(例如,使用标准库设施)更有效。通常(但当然不是总是),情况正好相反。
19.3.2.2 void*
在C语言中, void* 可以作为赋值或初始化任何指针类型变量的右操作数;但在C++中则不可以。例如:
void f(int n)
{
int* p = malloc(n * sizeof(int)); /* 非C++代码;在C++中,应使用‘new’进行分配 */
// ...
}
这可能是最难处理的兼容性问题。注意,将 void* 隐式转换为不同的指针类型通常并不是无害的:
char ch;
void* pv = &ch;
int* pi = pv; // 非C++代码
*pi = 666; // 覆盖ch及其附近的字节
在两种语言中,都要将 malloc() 的结果转换为正确的类型。如果只用C++,应避免使用 malloc() 。
19.3.2.3 链接
C和C++可以实现(并且经常是)使用不同的链接约定。最基本的原因是C++更注重类型检查。一个实际的原因是C++支持重载,因此可以有两个名为 open() 的全局函数。这必须在链接器的工作方式中体现出来。
为了使C++函数具有C链接(以便可以从C程序片段中调用它),或者允许从C++程序片段中调用C函数,应将其声明为 extern "C" 。例如:
extern "C" double sqrt(double);
现在, sqrt(double) 可以从C或C++代码片段中调用。 sqrt(double) 的定义也可以编译为C函数或C++函数。
在一个作用域中,只有一个具有给定名称的函数可以具有C链接(因为C不允许函数重载)。链接规范不影响类型检查,因此C++的函数调用和参数检查规则仍然适用于声明为 extern "C" 的函数。
19.4 编年史
略
19.5 建议
- ISO C++标准[C++,2020]定义了C++。
- 在为新项目选择风格或现代化代码库时,请依赖C++核心指南;§19.1.4。
- 学习C++时,不要孤立地关注语言特性;§19.2.1。
- 不要拘泥于几十年前的语言特性集和设计技术;§19.1.4。
- 在生产代码中使用新功能之前,尝试通过编写小程序来测试您计划使用的实现的标准一致性和性能。为了学习C++,请使用您可以访问的最新和最完整的标准C++实现。
- C和C++的共同子集不是学习C++的最佳初始子集;§19.3.2.1。
- 避免类型转换;§19.3.2.1;[CG: ES.48]。
- 优先使用命名类型转换,如 static_cast ,而不是C风格的类型转换;§5.2.3;[CG: ES.49]。
- 当将C程序转换为C++时,将作为C++关键字的变量重命名;§19.3.2。
- 为了可移植性和类型安全,如果您必须使用C,请在C和C++的共同子集中编写;§19.3.2.1;[CG: CPL.2]。
- 当将C程序转换为C++时,将 malloc() 的结果转换为适当的类型,或者将所有 malloc() 的使用更改为 new 的使用;§19.3.2.2。
- 当从 malloc() 和 free() 转换为 new 和 delete 时,考虑使用 vector 、 push_back() 和 reserve() 代替 realloc() ;§19.3.2.1。
- 在C++中,没有从整数到枚举的隐式转换;在必要时使用显式类型转换。
- 对于每个在全局命名空间中放置名称的标准C头文件 <X.h> ,头文件 <cX> 将名称放置在命名空间 std 中。
- 在声明C函数时使用 extern "C" ;§19.3.2.3。
- 优先使用 string 而不是C风格的字符串(直接操作以零结尾的 char 数组);[CG: SL.str.1]。
- 优先使用 iostream 而不是 stdio ;[CG: SL.io.3]。
- 优先使用容器(例如, vector )而不是内置数组。