现代 C++ 及C++ 的演变

Blog
Author:
Amir KirshAmir Kirsh
Published On:
10月 11, 2021
Estimated reading time:
2 minutes

C++ 活跃在程序设计领域。该语言写入了许多新项目,而且据 TIOBE 排行榜数据显示,C++ 的受欢迎度和使用率位居第 4,仅次于 Python、Java 和 C。

尽管 C++ 在过去二十年里的 TIOBE 排名都位居前列(2008 年 2 月排在第 5 名,到2019 年 5 月上升至第 3 名),但在 20 年前,人们并不看好 C++ 未来的发展。

至少在 2001 年,可以肯定的是,C++ 的使用似乎呈下降趋势。Java 和 C# 等其他编程语言开始逐步占据领导地位。在网络开发方面,javascript 则遥遥领先,而当时 Python 甚至还没有彻底崛起。

这时候的 C++ 似乎正在经历一场中年危机(引自前面发布于 2001 年的一篇文章):

“……随着网络开发者周期不断紧缩,以及更易于使用的新编程语言不断涌现,C++ 似乎正经历着一场中年危机,它评估着自身优势,利用作为编程语言仅剩不多的时间来做它力所能及的事。征服世界已是往日烟云。但 C++ 知道自己擅长什么,也知道自己存在的意义,并且分内的工作也做得十分出色。”

即使从现在来看,虽然 C++ 很有可能已经跨越了中年危机的大山,但这段描述仍在很大程度上反映了事实——没有人认为 C++ 是可以用作所有用途的语言,而它自己显然也不再试图征服世界。C++ 的用户清楚它的优势——在竞争对手相对较少的领域中具有的优势。但在 20 年前,C++ 的中年危机似乎是一次巨大的挑战,而在该处境下的 C++ 似乎正在缓慢消亡,并且注定会被 Java 和 VB.NET 等语言所取代。

人们在 2015 2018 都提出了“C++ 是否正在没落?”或“C++ 还有价值吗?”等疑问,甚至在今天也是如此。没错,即使是 2021 年的今天仍有这样的疑问!但是别误会,上述所有文章的答案都是:“不,它没有没落”以及“是的,它仍有价值。”不过存在这样的问题就意味着人们对 C++ 的存在抱有疑虑。即使某篇博客文章没有将 C++ 列入即将衰亡的语言清单,部分读者也会毫不犹豫地 C++ 添加到这一清单中,而另一部分读者则会。

这种疑虑会存在这么多年的主要原因就是 C++ 比较复杂,而且一些人将它视作遗留语言。随着新语言的诞生,竞争也变得激烈起来,尤其是大多数新语言都更易于用来编写代码或者至少有人认为使用它们编写代码会更容易。而 C++ 却比较复杂。尽管有着许多与 C++ 相关的衰亡预言,它的使用频率却非常高,许多人预测它将在未来几年维持现有的地位

那么 C++ 是如何逆转这些衰亡预言的?

C++ 依旧具有影响力且将会继续保持其影响力的主要原因有两个。

首先,提供低级别控制、硬件内存访问和高运行时性能的语言为现实需求。从嵌入式和实时系统(包括航空、汽车、国防和军事系统、医疗器械等)到游戏、金融、大流量系统、深度学习引擎等众多行业和领域都离不开这些功能。

此外,C++ 充满了生机。它每分钟都在改善和进化(或许不是每分钟,但你懂我的意思)。这一进化过程正是本文的主题。如果 C++ 没有处在进化过程中,那么很有可能它已经开始衰亡。程序员都倾向于使用尖端技术和现代的、最新的技术。就软件编程语言而言,与其他编程语言相比,他们也更喜欢具有现代特征的语言。

C++ 的演变

这一切都要追溯到本贾尼·斯特劳斯特卢普 (Bjarne Stroustrup) 在 C 中编写复杂代码时的糟糕经历。在编码过程中缺少了“面向对象”的部分,也就是从“对象”角度思考的能力。1979 年,本贾尼实现了包含类的 C 语言 (C with Classes),之后经过不断发展,并在 1982 年将其更名为 C++。1985 年,C++ 程序设计语言》第一版出版。其实那时候就已经有几个编译器实现存在了,只是尚未标准化。本贾尼希望 C++ 能有一个官方标准,并受社区影响。这也进而激励他开始为 C++ 制定 ISO 认证标准,这一目标于 1997 年 11 月 14 日实现,经过对 C++ 标准的表决,最终诞生了 C++98。几年过后,ISO 委员会收集到语言规范中存在的小缺陷并发布了 C++03,而修正后的标准与 C++98 大体上无异。

之后很长一段时间内, C++ 再没有其他动作。

C++ 被广泛使用,并且几乎在所有计算机科学课程中都被当作一门先进的编程语言供人们学习。同时也出现了许多 C++ 库,包括大家熟知的Boost 库(实际诞生于 1999 年)。但是 C++ 本身却停滞不前。

而在这几年时间里,Java 和 C# 等受 C++ 影响的其他语言却越发活跃起来。这些语言引入了本可以在 C++ 中实现的特性,例如在类中初始化数据成员的能力:

/class Foo { 
 int widget = 42; // not allowed in C++98/03
public:
 // ...
};/

那时候告诉大家不能在类中初始化数据成员时给出的解释是,内存中还没有相应的位置,而只有在创建完对象后才会提供。因此,必须在构造函数主体或构造函数初始化列表中进行初始化。当然,这并非事实,因为受 C++ 影响的 Java 和 C# 就能实现这一功能。称内存中还没有分配字段的位置只不过是借口,因为编译器应该有能力使这一初始化过程仅在创建对象时进行。

你猜怎么着?在那之后过了十年,这一功能终于实现了。

这种语法问题似乎只是小问题,但开发人员却不这么认为,毕竟类似能力在其他语言中明明就能够实现。开发人员往往会比较各种软件语言,当他们错过似乎能派上用场的东西时就会发牢骚。

除了语法问题外,显然还需要扩展该语言的 C++ 库。

很明显,C++ 必须进化。但是,由于没有明确的道路,这一过程受到阻碍。

早在 2000 年代初,本贾尼就已做好让经过讨论的新标准 C++0x 在 2003 年到 2005 年间的某个时间点投入使用:

  • 语言本身没有重大变化
  • 对标准库做重要扩展

(详情见本贾尼 FAQ 中的内容)。

而实际的情况则有些许不同:新的 C++0x 标准台于 2011 (即十六进制 0xB),自然得名为 C++11。不过该标准确实包含了许多对 C++ 语言的补充,而不仅仅是对标准库的补充。

C++11

C++11 是 C++ 的主要版本和重要成果,内容类别如下:

  1. 新的“语法糖”功能,使代码更容易编写、阅读和维护。
  2. 对语言的修改和功能的添加,使编写或简化代码有了新的选择。
  3. 增加了实现编写更高效代码的功能。
  4. 增加了资源管理功能,减少易出现 bug 的代码。
  5. 增加了库扩展,减少对外部库的依赖。

更好的代码 -“语法糖

包括用于自动推断变量类型的”auto”关键字等。

/std::map<std::string, int> name_2_id;
 // ...
 auto itr = name_2_id.find("John Doe"); /

之所以被称作“语法糖”,是因为你可以用更冗长的语法实现完全相同的目标:

/std::map<std::string, int> name_2_id;
 // ...
 std::map<std::string, int>::iterator itr = name_2_id.find("John Doe");/

没有人会认为第二个代码更容易编写。可能会有人说,代码越长,阅读和维护起来也就更容易,但如今的观点是“auto”十分有帮助,与上述代码类似的情况下确实应该使用这一工具。

有趣的是,Java 和 C# 都有“var”关键字,相当于 C++ 的“auto”。然而,在“auto”被引入 C++ 约 6 年或 7 年后才在这两种语言中实现这一功能。C# 的“var”于 2017 年的第 3 版中引入,Java 则是在 2018 年的第 10 版中引入“var”。并且,C# 和 Java 中“var”的能力不如 C++ 中的“auto”强大

另一种“语法糖”是“基于范围的 for 语句”(range-based-for),在 C++11 中增加 :

/ for(const auto& name_id_pair: name_2_id) {
   // do something with pair of name and id
 } /

这也是“语法糖”,因为规范本身就以将被编译器替代的代码定义了基于范围的 for。

需要注意的是,“语法糖”依然可以帮助程序员编写正确的代码,如果不使用“语法糖”,可能会导致执行错误或低效的代码。可以在这里找到一个相关的例子,尤其是使用“auto”推导类型的情况。所以在某些情况下,这不仅仅是起到“加糖”的效果。

变更和附加功能

C++11 增加了一些重要功能,如 lambda 表达式可变参数模板,变更内容包括用“noexcept”替换“throw”签名等。

本文章并非旨在回顾不同 C++ 版本中的变更内容和新增特性,所以在此不会深入探讨相应内容(如需了解更多相关信息,可在文章末尾找到相应的链接)。但值得一提的是,C++ 也变得现代化,具备了其他语言中已有的功能,比如 lambda 表达式。

更高效的代码

C++11 引入的十分明智的一个功能就是右值和移动语义。这一补充彻底解决了 C++ 中冗余拷贝的问题,这也是 C 语言在嵌入式和实时环境中站稳脚跟的原因之一。在这些环境中,由于成本过高,几乎不可能实现额外的冗余拷贝。有了 C++11 右值和移动语义,多数冗余拷贝问题也就迎刃而解,并且性能得以显著提升,C 程序员因此纷纷开始借助 C++ 的东风

另一个提高效率的附加功能是 emplace。它允许在标准容器内创建对象而无需复制或移动对象。C++11 基于变体模板和完美转发在无需进行冗余拷贝的情况下即可实现构造函数参数的转发。

不易出现 bug 的代码

有了智能指针,C++11 便开创了一条“无需对用户代码进行增减”的新途径,实现零规则并贯穿无需收集垃圾的语言也能具备安全和简易资源管理模型的理念。而在 C++11 中增加智能指针一定程度上正是依赖右值和移动语义实现的。

标准库扩充

更丰富的标准库减少了对外部库的依赖,同时增加了不同项目间的共通性。C++11 为丰富的通用 C++ 库铺平了道路,支持多线程和并发,扩充了作为标准库的一部分提供的容器和算法选择。

现在我们已经有了 C++20

自 C++11 推出后,所有人都知道有必要定期更新 C++ 版本。当时还没有为 C++ 定下每三年一次的版本更新周期,但改进 C++ 的动力仍不断推动着该语言的发展。在 C++11 之后发布了小的更新版本 C++14。而 C++17 诞生的基本上确立了每三年更新一次的周期,于是又推出了 C++20。尽管内容存在些许不足,

但从 C++14 到 C++20 的三个 C++ 版本均在目标期限内完成,并按时发布。按时出版,把已经准备好的和票选出的内容放到新版本中并推迟其余特性成为新常态。

那么从 C++14 到 C++20 都哪些变化?

C++14 – 一次小而重要的更新

C++14 中增加了一些重要的功能,例如:

  • 增加了 std::make_unique。虽然 C++11 中增加了 std::make_shared,但 std::make_unique 却被忽略了。(在 C++11 中引入 std::make_shared 自然有着充分理由,而且人们对 std::make_unique 的需求并不像对 std::make_shared 的需求那样大,不过将 std::make_unique 作为标准库的一部分毫无疑问有着一定的意义)。
  • 函数返回类型推导:现在,你可以直接使用“auto”作为返回类型,编译器会从你的函数中推导出返回的类型。
/*auto foo() {
 return moo(); // if moo changes its return type, so do I*
}/
  • 通用 lambdas:允许 lambdas 获取“自动”参数类型,作为模板函数使用。
  • Lambda 捕获表达式:允许在 lambda 捕获结果中声明一个新变量。为实现将对象移动到 lambda 中,这一能力是必须的,而且省去了使用 std::bind 的必要

如需了解 C++14 中增加的其他功能,请查阅本文末尾链接中的内容。

C++17 – 全新的想法和扩展

C++17 引入了一些新的概念,实现为复杂用途编写更简单的代码:

  • 类模板参数推导 (CTAD),基于构造函数参数,在不提供模板参数的情况下创建模板类的对象,例如:
/std::tuple tup("C++", 17, "magic"); // no need to use std::make_tuple/
  • if-constexpr,用于编译时评估条件。实现编写根据编译时的条件返回不同类型的模板函数,而这在推出 C++17 前需借助专用化或重型 SFINAE 机器实现。请点击示例观看视频,了解使用 C++17 实现相应能力的方法。
  • 结构化绑定,使 C++ 更接近 Python,增加的语法如下:
/std::map<std::string, int> name_2_id;
 for(const auto& [name, id]: name_2_id) {
  // extracting name and id from a pair
 }/
  • template<auto> – 允许非类型模板参数应用“auto”,详情请点击阅读观看视频。https://www.youtube.com/watch?v=wNGEtlBSCLY&t=33s
  • 标准库添加:std::optionalstd::anystd::variantstd::string_view
  • 标准库中添加了新的 filesystem library

同样,此处只描述了 C++17 的部分特性。如需了解 C++17 中增加的其他功能,请查阅本文末尾链接中的内容。

C++20 – C++ 的飞跃

shutterstock_1390839050_Modern-C

C++20 将 C++ 的能力推向一个新的高度,更新的四大新特性已在该博客文章中详细介绍:

  • 模块 – 实现代码模块化新途径,不再使用原来的 #include,具有明显优势,减少依赖的同时优化了构建时间。
  • 概念 – 大家期盼已久的功能特性,允许在不使用 SFINAE 的情况下限制模板参数,实现美观的鸭子类型设计风格。
  • 协程 – 许多其他语言中已经存在的机制,用于实现自然异步调用。
  • 范围 – 替代在算法中使用迭代器的巧妙设计,实现更简单的标准算法调用和先进的容器及视图链接能力。

除了这四大特性外,还有一些其他重要的扩展,如三路比较(宇宙飞船运算符)、许多 lambdas 相关的新能力、新的标准属性、取消某些情况下对 tyename 的依赖等等。

与前面的内容一样,以上仅包括该版本的部分特性。如需了解 C++20 中增加的其他功能,请查阅本文末尾链接中的内容。

向新版本 C++ 进行项目迁移

保持使用最新版本的 C++ 通常就是最好的办法,因为新版会提供新的特性和更先进的库。99% 的 C++ 版本都支持向后兼容(规范中存在少数修复缺陷可能会破坏兼容性的情况,但这些情况非常罕见且十分有限)。在彻底使用新版本前,你可能需要稍作停留,以确保符合要求的编译器表现良好,但不管怎样最终都必须进行过渡。如需了解有关迁移项目管理的更多内容,请查阅博客文章:如何实现遗留 C++ 代码的现代化?

当你开始学习任何新版本的 C++ 时,千万不能囫囵吞枣。而是应该一个中位,一个字节,慢慢消化。保持冷静,现代 C++ 并不可怕

委员会

这篇文章介绍了实际推动 C++ 发展的委员会工作。C++ 的发展以针对 C++ 社区提交的建议进行讨论所产生的结果为基础。此类提案可能包括拟议的新特性(例如,推导 this)、针对标准库扩充内容的建议(例如,执行器库)以及报告标准库缺陷和针对性地提出修改建议(例如,counted_iterator 问题)。

从下面几张照片也可以看出,多年来,委员会的规模在不断扩大(图片来自委员会网页)。光是管理这样的团队和做出一致决定就已经非常不容易,要让整个团队达成共识更是一大挑战。然而以上两点却都成功做到了。委员会最重要的职责之一是确保 C++ 的任何新增内容都不会破坏现有的代码。

modern-C_committee-has-grown

现代 C++ – C++23 的规划

按照每 3 年推出一版的计划,C++23 便是下一个版本,而且是小版本。受新冠肺炎疫情影响, ISO 委员会只能开展线上会议,无法面对面交流对版本开发进度亦有一定影响。虽然 C++23 仍将引入一些令人期待的内容,但确切内容尚不清楚。

目前看来,C++23 更多是与问题修复和添加小特性相关的更新。本贾尼希望在 C++23 中实现的功能(他在 CoreCpp 中提到的内容)包括:(a) 在标准库中增加协程支持;(b) 让标准库使用模块;(c) 执行器,用于管理并发和并行执行任务的标准库解决方案。不过目前还不知道上述内容能否如期在 C++23 中实现。毕竟 C++23 的静态反射候选方案已预计推迟到 C++26 中实现。

那么,C++ 未来的发展方向是什么?

Jon Kalb 和 Gašper Ažman 在《C++ Today:The Beast is Back》一书中(O’Reilly 2015,免费下载)概述了 C++ 的未来(第 69 页),并着重声明“永远不要做预测,尤其是与未来会怎样有关的预测”(引用了卡西·史丹格尔,也有可能是尼尔斯·玻尔或其他人说过的话),书中内容主要围绕以下特性展开:

性能

移动和云计算让人们重新拾起对性能的兴趣——性能将会一直占据重要地位,而这对于在性能方面从不妥协的语言来说是件好事。

新平台

随着硬件成本降低,涌现出越来越多的计算设备,并且其中一些设备对内存和占位面积的要求非常严格。而这对于一种高度便携的系统语言而言十分有利,因为这种语言在功能方面采取的是“无需为不使用的东西付费”的策略。

扩展

在高端领域,硬件成本降低将导致系统设计和实现的规模达到我们目前难以想象的水平。为实现这些系统,工程师们将会寻找一种可以随着用户和团队规模增加实现扩展,并支持创建大型系统的语言。这样的语言需具备足够高的性能,同时能支持相应规模的系统设计所需的高级抽象概念。此外,它还需要尽可能多的编译器辅助 bug 捕获,这在很大程度上得益于 C++ 支持的表达式类型系统。

软件普遍性

我们的世界正在不断演变成一个软件无处不在的世界。但我们是否需要所有软件都具有高度便捷、低内存、高性能代码的特点?当然不。现实中对许多应用程序的需求都不需要性能极高的软件。不过这些应用程序始终运行在对性能有着一定要求的基础设施上。目前很多这样的基础设施都基于 C 语言编写,而当基础设施的代码需要高层次的抽象时,无论现在还是将来,这些代码通常都是用 C++ 编写的。

[……]事实上,高性能的基础设施使得在要求不高的编程环境中创建应用程序成为可能。更多的程序员使用高级非系统语言工作只会继续增加对系统编程项目的需求和此类项目的价值,毕竟他们的工作正是在此基础上进行。

强大的工具

C++ 的理念是更多地依靠强大的编译器来完成制作高性能应用程序的重任。[……]计算技术的世界可能会发生迅速、戏剧性、时而出乎意料的变化,但从我们目前所处的角度来看,C++ 似乎会在可预见的未来继续扮演着重要的角色。

于 2015 年发表的《The Beast is Back》在今天依然有着重要意义。加上 C++ 的不断发展,围绕它建立起的社区也充满活力,你所获得的是一个看起来十分现代化、年轻和称心如意的中年语言。鉴于 C++ 多年来的受欢迎程度,有人预测,即使所有 C++ 程序员都被人工智能取代,C++ 也将继续存在。

附加信息阅读链接

C++ 语言版本新增特性的维基百科列表:

C++11C++14C++20C++23 规划

ISO 最新 C++ 状态汇总

有关 C++ 历史的 CppReference 页面

C++11 相关链接:

C++11 概述, C++ FAQ(最初由本贾尼·斯特劳斯特卢普创建,相关链接)。

Stackoverflow 博文:C++11 中有哪些突破性变化?

Stackoverflow 博文:C++11 新特性

Stackoverflow 博文:什么是移动语义?

Stackoverflow 博文:C++11 中的 lambda 表达式是什么?

Stackoverflow 博文:C++11 标准化内存模型

C++17 C++20 相关链接:

Stackoverflow 相关博文:C++17 新特性

Incredibuild 博文:C++20 新特性

Microsoft 博文相关总结:现代 C++ 的发展

ISO 关于各版本变更内容的文件:

C++11 C++14 的差别

C++14 C++17 的差别

C++17 C++20 的差别

WhitepaperDownload