本节我们来查看一下在二十一世纪的第二个十年里,C++ 如何被使用,以及用来做什么:
C++ 的使用领域绝大部分与 2006 年相同(§2.3)。虽然有一些新的领域,但在大多数情况下,我们看到的 C++ 还是在相同或类似的领域中被更加广泛和深入地使用。C++ 没有突然成为一种面向 Web 应用开发的语言,虽然即使在那种场景下仍有人用 C++ [Obiltschnig et al. 2005]。对于大多数程序员来说,C++ 依然是某种隐没在后台的东西,稳定、可靠、可移植、高性能。最终用户是看不见 C++ 的。
编程风格则有更加巨大的变化。比起 C++98,C++11 是门好得多的语言。它更易于使用,表达能力更强,性能还更高。2020 年发布的 C++20 则在 C++11 的基础上做出了类似程度的改进。
大致而言,C++ 可谓无处不在、无所不用。但是,大象无形,大多数 C++ 的使用并不可见,被深深隐藏在重要系统的基础设施内部。
C++ 被用在哪里,是如何被使用的,没人能够完整了解。2015 年,捷克公司 JetBrains 委托进行了一项研究 [Kazakova 2015],结果显示在北美、欧洲、中东以及亚太地区 C++ 被大量使用,在南美也有一些使用。“在南美的一些使用”就有 40 万开发者,而 C++ 开发者的总人数则达到了 440 万。使用 C++ 的行业有(按顺序)金融、银行、游戏、前台、电信、电子、投资银行、营销、制造和零售。所有迹象表明,自 2015 年以来,C++ 的用户数量和使用领域一直在稳步增长。
在这里,我将对 2006 到 2020 年期间内 C++ 的应用领域给出一个可能有些个人化的、印象派的、非常不完整的概览:
- 工业界:电信(例如 AT&T、爱立信、华为和西门子)、移动设备(基本上是所有,信号处理、屏幕渲染、对性能或可移植性有重大要求的应用)、微电子(例如 AMD、英特尔、Mentor Graphics 和英伟达)、金融(例如摩根士丹利和文艺复兴)、游戏(几乎所有)、图形和动画(例如 Maya、迪士尼和 SideFx)、区块链实现(例如 Ripple)、数据库(例如 SAP、Mongo、MySQL 和 Oracle)、云(例如谷歌、微软、IBM 和 Amazon)、人工智能和机器学习(例如 TensorFlow 库)、运营支持(例如 Maersk 和 AT&T)。
- 科学:航空航天(例如 Space X、火星漫游者、猎户座载人飞行器、詹姆斯·韦伯太空望远镜)、高能物理(例如 CERN 欧洲核子研究中心、SLAC 国家加速器实验室、费米实验室)、生物学(遗传学、基因组测序)、超大规模计算。
- 教学:全球大多数工程院校。
- 软件开发:TensorFlow、工具、库、编译器、Emscripten(从 C++ 生成 asm.js 和 WebAssembly)、运行期代码生成、LLVM(许多新语言的后台支柱,也大量用于工具构建中)、XML 和 JSON 解析器、异构计算(例如 SYCL [Khronos Group 2014–2020] 和 HPX [Stellar Group 2014–2020])。
- Web 基础设施:浏览器(Chrome、Edge、FireFox 和 Safari)、JavaScript 引擎(V8 和 SpiderMonkey)、Java 虚拟机(HotSpot 和 J9)、谷歌和类似组织(搜索、map-reduce 和文件系统)。
- 主要 Web 应用:阿里巴巴、Amadeus(机票)、Amazon、苹果、Facebook、PayPal、腾讯(微信)、Yandex。
- 工程应用:达索(CAD/CAM)、洛克希德·马丁(飞机)。
- 汽车:辅助驾驶 [ADAS Wikipedia 2020; Mobileye 2020; NVIDIA 2020]、软件架构 [Autosar 2020; Autosar Wikipedia 2020]、机器视觉 [OpenCV 2020; OpenCV Wikipedia 2020]、宝马、通用、梅赛德斯、特斯拉、丰田、沃尔沃、大众、Waymo(谷歌自动驾驶汽车)。
- 嵌入式系统:智能手表和健康监控器(例如佳明)、相机和视频设备(例如奥林巴斯和佳能)、导航辅助设备(例如 TomTom)、咖啡机(例如 Nespresso)、农场动物监控器(例如 Big Dutchman)、生产线温度控制(例如嘉士伯)。
- 安全:卡巴斯基、美国国家安全局、赛门铁克。
- 医疗和生物学:医学监测和成像(例如西门子、通用电气、东芝和飞利浦)、断层扫描(例如 CT)、基因组分析、生物信息学、放射肿瘤学(例如 Elekta 和 Varian)。
虽然这只是冰山一角,但它展示了 C++ 使用的广度和深度。大多数 C++ 的使用对其(间接)用户不可见。某些对 C++ 的使用早于 2006 年,但也有很多是之后才开始的。没有一个主要现代系统只用单一语言写就,但是 C++ 在所有这里提到的应用场合中发挥了重要作用。
我们常常忘记那些平凡的却在我们的生活中起着重要作用的应用。没错,C++ 可以帮助运行美国国家航空航天局的深空网络,但也可以在人们日常熟悉的小设备中运行,例如咖啡机、立体声扬声器和洗碗机。让我惊讶的是,C++ 竟然也被应用于运转现代养猪场的先进系统中。
与 2006 年相比,2020 年的 C++ 社区更加壮大,不断蓬勃发展、积极向上、富有成效,并且急切地想看到未来的进一步改进。
与大多数编程语言社区相比,C++ 社区一向是出奇地无组织和分散。这个问题早已有之,因为我就没有建立组织的才能。当时我的雇主 AT&T 贝尔实验室并不想建立一个 C++ 社区,但是似乎其他所有人都非常感兴趣,并且愿意花钱来建立他们的用户群。最终的结果是,许多公司,例如苹果、Borland、GNU、IBM、微软和 Zortech 都建立了以其客户为中心的 C++ 社区,但是却没有总体的 C++ 社区,社区没有中心。有杂志,读的人不多(相对于 C++ 社区的规模)。虽然有会议,但它们倾向于被一般的“面向对象”的会议或“软件开发”的会议所吸收或者就演变成了那些一般性会议。没有总体的 C++ 用户组。
如今,世界上有数十个本地、国家和国际 C++ 用户组,这些用户组之间也经常进行一些合作。除此之外,还有数十个 C++ 会议,每个会议都有数百人参加:
- C++ 基金会——成立于 2014 年,是一家非盈利性组织,旨在推广 ISO C++(而不是任何特定供应商的 C++),它主办 CppCon 年度会议。
- Boost——成立于 1999 年,它是一组经过同行评审的库、以及建造使用它们的社区。Boost 举行年度会议。
- Meeting C++——成立于 2012 年,是一个非常活跃的用户团体网络,定期举行会议(最初在德国活跃)。在不同地方有数十个 Meeting C++ 的会议和聚会。
- ACCU——成立于 1984 年,最初作为 C 用户组而建立,是所有现存 C++ 组织中的爷爷辈了;它出版两本杂志,并举行年度会议(主要在英国活跃)。
- isocpp.org——C++ 基金会的网站,其中包含与 C++ 有关的新闻,标准化进程相关的信息,以及有用的链接。
- cppreference.com——出色的在线参考资料;它甚至有一个历史部分!
- 会议——CppCon、ACCU、Meeting++、C++ Now(以前称为 BoostCon)、Qt、NDC、std::cpp 的会议,以及在波兰、俄罗斯、中国、以色列和其他地方的一些会议。此外,很多通用软件会议上也在越来越多的安排 C++ 专题。
- 博客——有许多,播客也是。
- 视频——视频已成为有关 C++ 的最新进展的主要信息来源。主要的 C++ 会议通常会录制演讲视频并将其发布以供免费访问(例如 CppCon、C++Now 和 Meeting++)。视频采访已变得很流行。最多最受欢迎的托管网站是 YouTube,但不幸的是,YouTube 在有些拥有大型 C++ 开发者群体的国家(例如中国)被封了。
- GitHub——使共享代码和组织联合项目开发变得更加容易。
跟某些语言和供应商的集中组织相比,这还差得很远。但是,这些 C++ 社区和组织富有活力,彼此保持联系,并且比在 2006 年的时候活跃得多。此外,一些企业的用户组和会议也仍然活跃。
从 2006 年不太理想的状态(§2.3)到现在,C++ 的教育是否得到了改善?也许吧,但是对于 C++ 来说,教育仍然不是强项,大多数教育还都集中在为业内人士提供信息和培训上。在大多数国家/地区,很多大学毕业生对 C++ 语言及使用它的关键技术只能算一知半解。对于 C++ 社区来说,这是一个严重的问题。因为,对于一门语言来说,如果没有热情洋溢的程序员们源源不断、前赴后继地精通其关键设计和实现技术,那它是无法在工业规模上取得成功的。假如更多使用 C++ 的开发者知道如何更好地使用它,那他们就能做太多太多的事来改进软件!如果毕业生带着更准确的 C++ 视角进入工作岗位,那么太多太多的事情会变得容易得多!
C++ 教学所面临的一个问题是教育机构经常将编程视为低级技能,而不是基础课目。好的软件对我们的文明至关重要。为了把控软件,我们需要像对待数学和物理学一样,严肃认真地对待关键系统的软件开发。那种削足适履的方式对于教育和软件开发是行不通的。一个学期的教学也远远不够。我们永远都不会期望在教了短短几个月英语之后,学生就会懂得欣赏莎士比亚。同样,了解语言的基本机制与精通内行所使用的惯用法和技巧之间是有差距的。就像任何主要的现代编程语言一样,教授 C++ 也需要根据学生的背景和需求相应地调整教学方法。即使教育机构意识到了这些问题并愿意做出一些弥补,奈何学生已经课满为患,教师也很难保持不跟工业实践脱节。SG20(教育)正试图总结教授和使用现代 C++ 的方法来提供一些帮助。SG15(工具)则可能提供更多支持教学的工具,从而越来越多地发挥重要作用。
从 C++11 开始,我们对此有了越来越多的认识。例如,Kate Gregory 制作了一些很棒的视频,介绍了如何教授 C++ [Gregory 2015, 2017, 2018]。最近的一些书籍认识到在支持教育方面,不同的受众存在不同的需求,并试图迎头解决这些问题:
- 《C++ 程序设计原理与实践》(Programming: Principles and Practice Using C++)[Stroustrup 2008a]——这是一本针对刚入门的大学生和自学人士的教科书。
- 《C++ 语言导学》(A Tour of C++)[Stroustrup 2014d,2018f]——针对经验丰富的程序员的简短概述(200 页)。
- 《发现现代 C++》(Discovering Modern C++)[Gottschling 2015]——这是一本专门为数学背景较强的学生准备的书。
我也写了一些半学术性质的论文(Software Development for Infrastructure [Stroustrup 2012] 和 What should we teach software developers? Why? [Stroustrup 2010b]),并在 CppCon 2017 开幕式上作了关于 C++ 教育的主题演讲(Learning and Teaching Modern C++ [Stroustrup 2017c])。
自 2014 年左右以来,视频和在线课程的使用急剧增加。这对 C++ 的教学来说很有帮助,因为这样就不需要一个中心组织或大量资金的支持。
以下列出了从 2006 到 2020 年间,与 C++ 语言相关的学术研究成果:
- 概念:泛型编程 [Dehnert and Stepanov 2000]、C++0x 概念 [Gregor et al. 2006]、使用模式 [Dos Reis and Stroustrup 2006]、库设计 [Sutton and Stroustrup 2011]。
- 理论与形式化体系:继承模型 [Wasserrab et al. 2006]、模板和重载 [Dos Reis and Stroustrup 2005a]、模板语义 [Siek and Taha 2006]、对象布局 [Ramananandro et al. 2011]、构造和析构 [Ramananandro et al. 2012]、用于代码处理的表示形式 [Dos Reis and Stroustrup 2009,2011]、资源模型 [Stroustrup et al. 2015]。
- 动态查找:快速动态类型转换 [Gibbs and Stroustrup 2006]、模式匹配 [Solodkyyet et al. 2012]、多重方法 [Pirkelbauer et al. 2010]。
- 静态分析:可靠的表示法 [Yang et al. 2012]、实践经验 [Bessey 2010]。
- 性能:代码膨胀 [Bourdev and Järvi 2006,2011]、异常实现 [Renwicket et al. 2019]。
- 语言比较:泛型编程 [Garcia et al. 2007]。
- 并发和并行编程:内存模型 [Batty et al. 2013,2012,2011]、HPX(一个适用于任何规模的并行和分布式应用程序的通用 C++ 运行时系统 [Kaiser et al. 2009Sept])、STAPL(自适应泛型并行 C++ 库 [Zandifar et al. 2014])、TBB(英特尔的任务并行库 [Reinders 2007])。
- 协程:数据库优化 [Jonathan et al. 2018; Psaropoulos et al. 2017]。
- 软件工程:代码的组织和优化 [Garcia and Stroustrup 2015]、常量表达式求值 [Dos Reis and Stroustrup 2010]
看起来还有更多的关于 C++ 的学术研究机会,关于语言的特性和技巧(例如,异常处理、编译期编程和资源管理),以及其使用的有效性(例如,静态分析或基于真实世界代码和经验的研究)。
C++ 社区中最活跃的成员中很少有人会考虑撰写学术论文,写书似乎更受欢迎(例如,[Čukić 2018; Gottschling 2015; Meyers 2014; Stepanov and McJones 2009; Vandevoorde et al. 2018; Williams 2018])。
与其他语言相比,在 1990 年代初期到中期,C++ 在用于工业用途的工具和编程环境方面做得相当不错。例如,图形用户界面和集成软件开发环境都率先应用于 C++。后来,开发和投资的重点转移到专属语言,例如 Java(Sun)、C#(微软)和 Objective-C(苹果)以及更简单的语言,例如 C(GNU)。
在我看来,有两个主要原因:
- 资金:组织倾向于使用他们可以控制的语言和工具,从而提供比竞争对手更大的差异化优势。从这个角度来看,C++ 由正式的标准委员会控制、强调所有人的利益,这反倒成了一个缺点——某种公地悲剧的变体。
- 宏和文本定义:C++ 没有一个简单,可广泛使用的内部表示形式来简化基于源代码的工具构建,并且大量使用宏必然导致程序员看到的跟编译器所分析的有所不同。和 C 一样,C++ 是根据字符序列来定义的,而非根据直接表示抽象且更易于操作的构件来定义。我与 Gabriel Dos Reis 一起定义了这样一个表示形式 [Dos Reis and Stroustrup 2009, 2011],但事实证明 C++ 社区中面向字符的传统难以克服。当初建造时没有意识到的规范化结构,想通过翻新加上去就难了。
因此,在 2006–2020 年期间,与其他语言相比,C++ 被支持工具方面的问题严重困扰。但是,随着以下这些工具的涌现,这种情况得到了稍许改善:
- 工业级的集成软件开发环境:例如微软的 Visual Studio [Microsoft 2020; VStudio Wikipedia 2020] 和 JetBrains 的 CLion [CLion Wikipedia 2020; JetBrains 2020]。这些环境不仅支持编辑和调试,还支持各种形式的分析和简单的代码转换。
- 在线编译器:例如 Compiler Explorer [Godbolt 2016] 和 Wandbox [Wandbox 2016–2020]。这些系统允许从任何浏览器中编译 C++ 程序,有时甚至可以执行。它们可用于实验,检查代码质量,还有比较不同的编译器及编译器和库的不同版本。
- GUI 库和工具:例如 Qt [Qt 1991–2020]、GTKmm [GTKmm 2005–2020] 和 wxWidgets [wxWidgets 1992–2020]。不幸的是,Qt 依赖于元对象协议(meta-object protocol,缩写为 MOP),因此 Qt 程序还不是标准的 ISO C++ 应用。静态反射(§9.6.2)使我们最终能够解决这个问题。C++ 社区的问题不是没有好的 GUI 库,而是太多了,因此会有选择困难。
- 分析器:例如 Coverity [Coverity 2002–2020],Visual Studio 的 C++ Core Guidelines 分析器(§10.6)和 Clang Tidy [Clang Tidy 2007–2020]。
- 编译器工具支持:例如 LLVM 编译器后端基础设施,可简化代码生成和代码分析 [LLVM 2003–2020]。除了 C++ 本身,这为许多新语言提供了福利。
- 构建系统:例如 build2 [Build2 2014–2020] 和 CMake [CMake 2000–2020],以及 GNUmake[GNUmake 2006–2020]。同样,在没有标准的情况下,选择会有困难。
- 包管理器:例如 Conan [Conan 2016–2020] 和 vcpkg [vcpkg 2016–2020]。
- 运行时环境:例如 WebAssembly:将 ISO C++ 编译为字节码以在浏览器中部署的系统 [WebAssembly 2017–2020]。
- 运行时编译、JIT 和链接:例如 Cling [Cling 2014–2020; Naumann 2012; Naumann et al. 2010] 和 RC++ [RC++ 2010–2020]。
上面列出的只是一些示例。像往常一样,C++ 用户面临的问题是可选方案的数量众多,例如:[RC++ 2010–2020] 列出了 26 个用于在编译时生成代码的系统,并且有数十个程序包管理器。因此,我们需要的是某种形式的标准化。
截至 2020 年,工具仍不是 C++ 的强项,但我们正在大范围内取得进展。
针对大多数现实问题的最佳解决方案需要组合使用多种技术,这也是 C++ 演进的主要动力。自然地,这让那些声称拥有单个简单最佳解决方案(“编程范式”)的人感到不爽,但是支持多种风格一直是 C++ 的根本优势。考虑一下“绘制所有形状”的例子,这个例子自 Simula 发展早期(绘图设备为湿墨绘图仪)以来就一直用于说明面向对象编程。用 C++20,我们可以这样写:
void draw_all(range auto& seq)
{
for (Shape& s : seq)
s.draw();
}
该段代码是什么编程范式?
- 显然,它是面向对象编程:使用了虚函数和类层次结构。
- 显然是泛型编程:使用了模板(通过使用
range
概念进行参数化,我们得到一个模板)。 - 显然,这是普通的命令式编程:使用了
for
循环,并按照常规f(x)
语法定义了一个将要被调用的函数。
对这个例子我可以进一步展开:Shape
通常具有可变状态;我可以使用 lambda 表达式,也可以调用 C 函数;我可以用 Drawable
的概念对参数进行更多约束。对于各种“更好”的定义,适当的技术组合比我能想到的任何一种单一范式所能提供的解决方案更好。
C++ 支持多种编程风格(如您坚持,也可以称为“范式”),其背后的想法并不是要让我们选择一种最喜欢的样式进行编程,而是可以将多种风格组合使用,以表达比单一风格更好的解决方案。
在 2006 年,许多 C++ 代码仍然是面向对象的风格和 C 风格编程的混合体。自然而然的,到 2020 年仍然有很多类似这样的代码。但是,随着 C++98 的到来,STL 风格的泛型编程(通常称为 GP)变得广为人知,并且用户代码也逐渐开始使用 GP,而不只是简单地使用标准库。C++11 中对 GP 的更好支持为在生产代码中更广泛的使用 GP 提供了极大的便利。但是,C++17 中缺少概念(§6),这仍然阻碍了 C++ 中泛型编程的使用。
基本上,所有专家都阅读过 Alex Stepanov 的《编程原本》(Elements of Programming,通常称为 EoP)[Stepanov and McJones 2009],并受到其影响。
基于模板的泛型编程是 C++ 标准库的支柱:容器、范围(§9.3.5)、算法、iostream、文件系统(§8.6)、随机数(§4.6)、线程(§4.1.2)(§9.4)、锁(§4.1.2)(§8.4)、时间(§4.6)(§9.3.6)、字符串、正则表达式(§4.6)和格式化(§9.3.7)。
C++ 中的元编程出自泛型编程,因为两者都依赖于模板。它的起源可以追溯到 C++ 模板的早期,当时人们发现模板是图灵完备的 [Vandevoorde and Josuttis 2002; Veldhuizen 2003],并以某种有用的形式提供编译期纯函数式编程。
模板元编程(通常称为 TMP)往往非常丑。有时,这种丑陋通过使用宏来掩盖,从而造成了其他问题。TMP 几乎无处不在,这也证明了它确实有用。例如,如果没有元编程,就无法实现 C++14 标准库。许多技巧和实验在 2006 年前就有了,但是 C++11 具有更好的编译器、变参模板(§4.3.2)和 lambda 表达式(§4.3.1),这些推动了 TMP 成为主流用法。C++ 标准库还增加了更多元编程的支持,比如:编译期选择模板 conditional
,允许代码依赖于类型属性的类型特征(type trait)如“能否安全地按位复制类型 X?”(§4.5.1),还有 enable_if
(§4.5.1)。举例来说:
conditional<(sizeof(int)<4),double,int>::type x; // 如果 int 小,就用 double
计算类型以精确地反映需求,这可以说是 TMP 的本质。我们还可以计算值:
template <unsigned n>
struct fac {
enum { val = n * fac<n-1>::val };
};
template <>
struct fac<0> { // 0 的特化:fac<0> 为 1
enum { val = 1 };
};
constexpr int fac7 = fac<7>::val; // 5040
注意,模板特化在其中起着关键作用,这一点在大多数 TMP 中是必不可少的。它已用于计算复杂得多的数值,也可以表示控制流(例如,在编译期计算决策表,进行循环展开,等等)。在 C++98 [Stroustrup 2007] 中,模板特化是一个很大程度上没有得到足够重视的特性。
在设计精巧的库中以及在现实世界的代码中,诸如 enable_if
之类的原语已成为数百甚至数千行的程序的基础。TMP 的早期示例包含一个完整的编译期 Lisp 解释器 [Czarnecki and Eisenecker 2000]。此类代码极难调试,而维护它们更是可怕的差事。我见识过这样的情形,几百行基于 TMP 的代码(不得不承认非常聪明),在一台 30G 内存的计算机上编译需要好几分钟的时间,由于内存不足而导致最终编译失败。即使是简单的错误,编译器的错误信息也可以达到几千行。然而,TMP 仍被广泛使用。理智的程序员发现,尽管 TMP 有着各种问题,仍比起其他方案要好。我见过 TMP 生成的代码比我认为一个合格的人类程序员会手写的汇编代码要更好。
因此,问题变成了如何更好地满足这种需求。当人们开始把像 fac<>
这样的代码视为正常时,我为此而感到担心。这不是表达普通数值算法的好方法。概念(§6)和编译期求值函数(constexpr
(§4.2.7))可以大大简化元编程。举例来说:
constexpr int fac(int n)
{
int r = 1;
while (n>1) r*=n--;
return r;
};
constexpr int fac7 = fac(7); // 5040
这个例子说明,当我们需要一个值时,函数是最佳的计算方式,即使——尤其——在编译期。传统模板元编程最好只保留用于计算新的类型和控制结构。
Jaakko Järvi 的 Boost.Lambda [Järvi and Powell 2002; Järvi et al. 2003a] 是 TMP 的早期使用案例,它帮助说服了人们 lambda 表达式是有用的,并且他们需要直接的语言支持。
Boost 元编程库 Boost.MPL [Gurtovoy and Abrahams 2002–2020] 展示了传统 TMP 的最好和最坏的方面。更现代的库 Boost.Hana [Boost Hana 2015–2020] 使用 constexpr
函数。WG21 的 SG7(§3.2)试图开发一种更好的标准元编程系统,其中还包括编译期反射(§9.6.2)。
我对 C++ 语言的最终目标是:
- 使用和学习上都要比 C 或当前的 C++ 容易得多
- 完全类型安全——没有隐式类型违规,没有悬空指针
- 完全资源安全——没有泄漏,不需要垃圾收集器
- 为其构建工具要相对简单——不要有宏
- 跟当前 C++ 一样快或更快——零开销原则
- 性能可预测——适用于嵌入式系统
- 表达力不亚于当前的 C++——很好地处理硬件
这和《C++ 语言的设计和演化》[Stroustrup 1994] 及更早版本中阐述的设计目标并没有太多不同。显然,这是一项艰巨的任务,并且与较旧的 C 和 C++ 的多数用法不兼容。
最早,在 C++ 还是“带类的 C”的时候,人们就建议创建语言的安全子集,并使用编译器开关来强制执行这种安全性。但是,由于许多原因中的某一个原因,这些建议失败了:
- 没有足够的人在“安全”的定义上达成一致。
- 不安全特性(对每种“不安全”的定义来说)是构建基本安全抽象的基础。
- 安全子集的表达能力不足。
- 安全子集效率低下。
第二个原因意味着,你不能仅仅通过禁止不安全的功能来定义一个安全的 C++。“通过限制以达到完美”这个方法,对于编程语言的设计来说,在极其有限的场合下才能发挥作用。你需要考虑那些一般来说不安全但有安全用途的特性的使用场景和特征。此外,该标准不能放弃向后兼容(§1.1),所以我们需要一种不同的方法。
从一开始,C++ 就采用了不同的哲学 [Stroustrup 1994]:
让良好的编程成为可能比防止错误更重要。
这意味着我们需要“良好使用”的指南,而不是语言规则。但是,为了在工业规模上有用,指南必须可以通过工具强制执行。例如,从 C 和 C++ 的早期开始,我们就知道悬空指针存在的问题。例如:
int* p = new int[]{7,9,11,13};
// ...
delete[] p; // 删除 p 指向的数组
// 现在 p 没有指向有效对象,处于“悬空”状态
// ...
*p = 7; // 多半会发生灾难
虽然许多程序员已经开发出防止指针悬空的技术。但是,在大多数大型代码库中,悬空指针仍然是一个主要问题,安全性问题比过去更加关键。一些悬空的指针可以作为安全漏洞被利用。
在 2004 年,我帮助制定了一套用于飞行控制软件 [Lockheed Martin Corporation 2005] 的编码指南,这套指南接近于我对安全性、灵活性和性能的构想。2014 年,我开始编写一套编码指南,以解决这一问题,并在更广泛的范围内应用。这一方面是为了回应对用好 C++11 的实用指南的强烈需求,另外一方面是有人认为的好的 C++11 让我看着害怕。与人们交谈后,我很快发现了一个明显的事实:我并不是唯一沿着这样的路线思考和工作的人。因此,一些经验丰富的 C++ 程序员、工具制作者和库构建者齐心协力,与来自 C++ 社区的众多参与者一起启动了 C++ 核心指南项目 [Stroustrup and Sutter 2014–2020]。该项目是开源的(MIT 许可证),贡献者列表可以在 GitHub 上找到。早期,来自摩根士丹利(主要是我)、微软(主要是 Herb Sutter、Gabriel Dos Reis 和 Neil Macintosh)、Red Hat(主要是 Jonathan Wakely)、CERN、Facebook 和谷歌的贡献者都做出了突出贡献。
核心指南绝不是唯一的 C++ 编码指南项目,但却是最突出、最雄心勃勃的。它们的目标明确而清晰,那就是显著提升 C++ 代码的质量。例如,早在 Bjarne Stroustrup、Herb Sutter 和 Gabriel Dos Reis 的论文中 [Stroustrup et al. 2015] 就阐明了关于完全类型和资源安全的理想和基础模型。
为了实现这些雄心勃勃的目标,我们采用了一种“鸡尾酒式”的混合方法:
- 规则:一套庞大的规则集,意图在 C++ 里实现使用上的类型安全和资源安全,推荐那些已知的有效实践,并禁止已知的错误和低效的来源。
- 基础库:一组库组件,使程序员可以有效的编写低层次程序,而无需使用已知的容易出错的功能,并且从总体上为编程提供更高层次的基础。大多数组件来自标准库,其中一些来自以 ISO 标准 C++ 编写的小型指南支持库(Guidelines Support Library,GSL)。
- 静态分析:检测违规行为、并强制执行指南关键部分的工具。
这些方法中的每一种都有很长的历史,但是每一项都无法单独在工业规模上解决这些问题。例如,我是静态分析的忠实拥护者,但是如果程序员使用动态链接的方式在一个单独编译的程序中编写任意复杂的代码,那么我最感兴趣的分析算法(例如,消除悬空指针)是不能求解成功的。这里的“不能”是指“一般说来,理论上是不可能的”,以及“对于工业规模的程序而言在计算上过于昂贵”。
基本方式不是简单的限制,而是我称之为“超集的子集”或 SELL 的方法 [Stroustrup 2005]:
- 首先,通过库功能对语言进行扩展,从而为正确的使用语言奠定坚实的基础。
- 然后,通过删除不安全、易出错及开销过高的功能来设置子集。
对于库,我们主要依赖标准库的各个部分,例如 variant
(§8.3)和 vector
。小型指南支持库(GSL)提供了类型安全的访问支持,例如 span
可以提供在给定类型的连续元素序列上的带范围检查的访问(§9.3.8)。我们的想法是通过将 GSL 吸收到 ISO 标准库中,从而最终也就不需要它了。例如,span
已被添加到 C++20 标准库中。当时机成熟时,GSL 中对于契约的微弱支持也应当被合适的契约实现所替代(§9.6.1)。
为了能规模化,静态分析完全是局部的(一次仅一个函数或一个类)。最难的问题与对象的生命周期有关。RAII 是必不可少的:我们已经不止一次的看到,手动资源管理的方法在很多语言中都很容易出错。此外,也有很多现存的程序,以一种有原则的方式使用指针和迭代器。我们必须接受此类使用方式。要使一个程序安全很容易,我们只需禁止一切不安全的功能。然而,保持 C++ 的表现力和性能是核心指南的目标之一,所以我们不能仅仅通过限制来获得安全。我们的目的是一个更好的 C++,而不是一个缓慢或被阉割的子集。
通过阐明原则、让那些优秀的做法更加显而易见、以及对已知问题进行机械化检查,这些指南可以帮助我们把教学的重点放在那些让 C++ 更有效的方面。这些指南还有助于减轻对语言本身的压力,以适应最新的发展趋势。
对于对象的生命周期,主要有两个要求:
- 切勿指向超出范围的对象。
- 切勿访问无效的对象。
考虑以下“基础模型”论文中的一个例子 [Stroustrup et al. 2015]):
int glob = 666;
int* f(int* p)
{
int x = 4; // 局部变量
// ...
return &x; // 不行,会指向一个被销毁的栈帧
// ...
return &glob ; // 可以,指向某个“永远存在”的对象
// ...
return new int{7}; // 可以(算是可以吧:不悬空,
// 但是把所有者作为 int* 返回了)
// ...
return p; // 可以,来自调用者
}
指针指向已知会超过函数生命周期的对象(例如,作为参数被传递到函数中),我们可以返回它,但对于指向局部资源的指针就不行。在遵循该指南的程序中,我们可以确保作为参数的指针指向某资源或为 nullptr
。
为避免泄漏,上面示例中的“裸 new
”操作应当通过使用资源句柄(RAII)或所有权标注来消除。
如果指针所指向的对象已重新分配,则该指针会变为无效。例如:
vector<int> v = { 1,2,3 };
int* p = &v[2];
v.push_back(4); // v 的元素可能会被重新分配
*p = 5; // 错误:p 可能已失效
int* q = &v[2];
v.clear(); // v 所有的元素都被删除
*q = 7; // 错误:q 无效
无效检查甚至比检查简单的悬空指针还要困难,因为很难确定哪个函数会移动对象以及是否将其视为失效(指针 p
仍然指向某个东西,但从概念上讲已经指向了完全不同的元素)。尚不清楚在没有标注或非本地状态的情况下,静态分析器是否可以完全处理无效检查。在最初的实现中,每个将对象作为非 const
操作的函数都被假定为会使指针无效,但这太保守了,导致了太多的误报。最初,关于对象生命周期检查的详细规范是由 Herb Sutter [Sutter 2019] 编写的,并由他在微软的同事实现。
范围检查和 nullptr
检查是通过库支持(GSL)完成的。然后使用静态分析来确保库的使用是一致的。
静态分析设想最早是由 Neil Macintosh 实现的,目前已作为微软 Visual Studio 的一部分进行发布。有一些检查规则已经成为了 Clang 和 HSR 的 Cevelop(Eclipse 插件)[Cevelop 2014–2020] 的一部分。一些课程和书籍中都加入了关于这些规则的介绍(例如 [Stroustrup 2018f])。
核心指南是为逐步和有选择地采用而设计的。因此,我们看到其中一部分在工业和教育领域被广泛采用,但很少被完全采用。要想完全采用,良好的工具支持必不可少。