C++编程之C++语言的发展
2024年12月5日,美国国家工程院、ACM、IEEE 院士、C++ 之父 Bjarne Stroustrup 在「2024 全球 C++ 及系统软件技术大会」上发表了题为《重新认识 C++:跨世纪的现代演进》的演讲。屏幕上,演示文稿的第一页就令人印象深刻:“C++ 几乎可以实现我们所期望的一切!”
从构建操作系统到开发高性能游戏引擎,从支持人工智能框架到驱动航天器控制系统,C++ 一直是系统级软件开发的首选语言。
当代 C++ 的简洁之美
谈到 C++ 的发展历程,Stroustrup 指出:“一些关键特性和技术已有多年历史,比如带构造函数和析构函数的类、异常处理机制、模板、std::vector……等等。另一些则是较新的发展,如 constexpr 函数和 consteval 函数、lambda 表达式、模块、概念、std::shared_ptr……等等。关键在于将这些特性作为一个整体来运用。”
“不要盲目使用所有新特性,也不要局限于仅使用新特性,”他强调道,“如果想了解最新特性和未来发展方向的更多细节,可以参考相关的技术讨论视频。我更关注的是如何将语言作为一个整体来开发好的软件。因为最终编程语言的价值体现在其应用程序的质量之中。”
资源管理:C++ 的基石
“我们知道,相比归还东西,人们更倾向于获取东西,”Stroustrup 首先打了个生动的比方,“问任何一个图书管理员就知道了,人们借书后常常忘记还书。在大型软件中,如果我们必须显式地返还借用的资源,我们肯定会遗漏一些。”
Stroustrup 将资源定义为“任何必须获取并在之后释放(归还)的对象”。“这包括内存、string(字符串)、互斥锁、文件句柄、套接字、线程句柄、着色器等等很多东西,”他解释道。“从这个词的含义来看,在编程中我们要处理的很多东西都是资源。”
在 C++ 中,每个资源(resource)都应该有对应的句柄(handle)来管理它的生存期。句柄负责资源的访问和释放,这种机制是通过对生存期的严格控制来实现的。
为了解决这个问题,Stroustrup 提出了几个关键原则:
避免手动释放资源——不要在应用程序代码中出现 free()、delete 等资源释放操作;
使用资源句柄——每个对象都由负责访问和释放的句柄管理;
基于作用域管理——所有资源句柄都属于特定作用域,可以在作用域间转移;
“这就是 C++ 的基石:构造函数(constructor)和析构函数(destructor),”Stroustrup 说道,“如果需要获取任何资源,那是构造函数的工作;如果需要归还资源,那是析构函数的工作。这里我们将抽象层次从机器级的指针和大小提升到了更高的层次。我们把它包装成一个类型,这个类型行为正确,有赋值操作,有访问函数,并且能正确清理。”
为了做好资源管理,Stroustrup 强调了对生存期的控制:
构造:首次使用前建立对象的不变量(如果有的话);
析构:最后使用后释放所有资源(如果有的话);
拷贝:a = b 意味着 a == b,且它们是独立的对象;
移动:在作用域间转移资源所有权;
“这些机制让我们能够开发出更安全、更可靠的代码,”他总结道,“因为资源管理不再依赖于程序员的记忆力,而是由语言机制自动保证。”
错误处理的策略
“在确保资源安全的基础上,我们还需要有明确的错误处理(error handling)策略,”Stroustrup 随即转入了另一个重要话题。他指出,C++ 中有两种主要的错误处理方式,它们各有适用场景:
“对于那些常见且可在局部处理的失败情况,使用错误码(error code)是合适的,这种方式避免了使用效率低下且丑陋的 try-catch 结构。”他解释了第一种情况,“但问题是,我们经常忘记检查错误码,这可能导致错误的结果继续传播。而且,这种方式不适用于构造函数和运算符。比如说,当你写 Matrix x = y + z 这样的表达式时,就没有地方放置错误返回语句和测试。”
“另一方面,对于那些罕见且无法在局部处理的错误,异常处理(exception handling)是更好的选择。”Stroustrup 继续说道,“错误可以沿调用链向上传播,避免陷入 ‘错误码地狱’。未捕获的异常会导致程序终止,而不是产生错误结果。重要的是,这种机制必须与 RAII(资源获取即初始化)配合使用,依赖作用域资源句柄。”
Stroustrup 还提到了一个最新的研究发现:“即便对小型系统,异常处理也可能比错误码更高效。我们最近看到一个很好的演示,展示了在小型固件中使用 C++ 异常可以产生更小、更快的代码。”
“关键是要记住,”他强调,“错误处理不是要选择唯一正确的方式,而是要根据具体情况选择最合适的方式。有时是错误码,有时是异常,重要的是要有一个明确的策略。即便对小型系统,异常处理机制也可能比错误码更高效。Khalil Estell 最近在 CppCon 2024 上的演示*展示了在小型固件中使用 C++ 异常可以产生更小、更快的代码。”
模块:打破“包含”的魔咒
谈到代码组织,Stroustrup 首先指出了一个困扰 C++ 开发者多年的问题:“头文件包含的顺序依赖问题一直是个麻烦。#include “a.h” 后跟 #include “b.h”,可能与顺序颠倒后的结果完全不同。这种基于文本的包含机制会导致:包含具有传递性、相同的代码被重复编译多次、容易引发宏定义冲突等问题。”
相比之下,C++20 引入的模块(modules)机制则完全不同,经过几十年,我们终于在 C++ 中实现了模块。我们不必再使用 include 了
泛型编程与概念
“泛型编程(generic programming)是当代 C++ 的关键基础,”Stroustrup 如此介绍道,“这个想法最早可以追溯到 80 年代初。那时我就描述过这个概念,只是当时我以为可以用宏来实现——关于这点我错了,但对需要泛型编程这一点我是对的。现代 C++ 中的大量泛型编程思想都来自 Alex Stepanov。”
泛型编程为 C++ 带来了多方面的优势:代码更加简洁、思想表达更直观、实现零开销抽象、保证类型安全。它在标准库中无处不在:容器和算法、并发、内存管理、I/O、string 和正则表达式等。
协程:状态保持!
“说到协程(coroutine),这其实是个有趣的故事,”Stroustrup 回忆道,“在 C++ 发展的最初十年,协程是我们的一个重要优势。但后来一些公司因为它不适合他们的机器架构而反对,结果我们失去了这个特性。现在,我们终于把它找回来了。”
“协程为我们提供了一种漂亮的方式来处理需要保持状态的计算,”他总结道。“它让代码更容易理解,也更容易维护。这正是我们一直追求的目标:简单的事情简单做。”
调优:“洋葱原则”
“对某些代码来说,调优是必要的,”Stroustrup 转入了性能优化的话题。“但我们都听过 ‘避免过早优化’ 这个建议。重要的是要在优化前后都进行性能测量,同时在设计接口时就要考虑优化空间。”
他提出了几个关键原则:
接口设计需明确定义
保持类型信息的完整性
提供足够信息支持检查和优化
管理复杂度:”简单的事情简单做!”
“我把这个叫做 ‘洋葱原则’,”Stroustrup 打了个生动的比方,“你可以把代码想象成洋葱的层。每当我们需要优化或处理特殊情况,我们就可能需要剥掉一层抽象。但要记住,每剥掉一层,你就会哭得更厉害。”
“为什么会这样?”他继续解释道,“因为每深入一层,你就有可能遇到更多的错误,必须写更多的代码,代码也更难理解。所以在真正需要之前,不要轻易剥掉一层抽象。这就是我对 ‘不要过早优化’ 的理解。”
指南和规格配置:走向未来
在标准委员会中,Stroustrup 和同事们正在推进一个更进一步的方案:规格配置(profile)。“每个规格配置是一套强制性的指南规则,”他解释道,“虽然现在还在制定中,但其目标很明确:让开发者能够根据需要选择不同类型的安全性级别和执行强度。这将帮助我们在保持语言强大的同时,使其更容易正确使用。”
Stroustrup 建议的初始规格配置包括:
- 算法:全面的范围检查,禁止解引用 end() 迭代器;
- 算术:检测上溢和下溢;
- 类型转换:全部禁用;
- 并发:消除死锁和数据竞争(这是个难点);
- 初始化:所有对象必须初始化;
- 失效:禁止通过已失效的指针访问(包括悬空指针);
- 指针:禁止对内置指针使用下标操作(应使用 span、vector、string 等);
- 范围:捕获范围错误;
- RAII:所有资源必须由句柄管理;
- 类型:涵盖初始化、范围、转换、失效和指针规则;
- 联合体:禁止使用 union(应使用 variant 等);
他说:“我们需要那些底层的、复杂的、接近硬件的、容易出错的、专家级的特性,因为它们是高效实现高层功能的基础。很多底层特性在正确使用时都很有价值。但一旦我们有了这些基础,就可以在此之上建立更安全、更简单的编程模型。”
“我们想要的是「增强版 C++」——简单、安全、灵活、高效,而不是功能受限的子集。我们不能失去 C++ 最重要的特性:高性能和对硬件的直接控制。而且这些改进不会改变语言的本质,最终的代码仍然是符合 ISO C++ 标准的。”
Stroustrup 还总结了 C++ 的编程模型:
静态类型系统,同时支持内置类型和用户定义类型
支持值语义和引用语义
统一的资源管理机制(RAII)
高效的面向对象编程
灵活且高效的泛型编程
编译期编程
直接访问硬件和操作系统
通过库实现的并发支持(借助内部指令)
最终淘汰 C 预处理器
C++的发展规模与优质软件
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出了OOP(objectoriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
C++的发展史
1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。
C++的发展就像是练功打怪升级一样,也是逐步递进,由浅入深的过程。我们先来看下C++的历史版本。
- C++98:这是第一个 ANSI/ISO 标准化的 C++ 版本,发布于 1998 年。它基于 Bjarne Stroustrup 于 1985 年创建的原始 C++ 设计,并加入了一些重要特性,如 STL(标准模板库)、异常处理、I/O Streams、命名空间和 RTTI(运行时类型识别)
- C++03:这个版本主要是对 C++98 的一些修正和改进,发布于 2003 年,并未引入新的语言特性,所以一般不把它当做重要版本,存在感也不强。
- C++11:这是 C++ 历史上最重大的更新之一,有时被称为 C++0x(因为它原计划在 200x 年发布,一直跳票)。它引入了大量新特性,如自动类型推断(auto 关键字)、基于范围的 for 循环、Lambda 表达式、智能指针、并发支持、移动语义、nullptr 和更强大的模板功能等。
- C++14:作为 C++11 的小幅度更新,C++14 引入了一些改进和新特性,包括泛型 Lambda 表达式、返回类型推导、二进制字面量、数字分隔符、弃用属性等。
- C++17:这个版本进一步提升了 C++ 的功能和易用性,新功能不是很多,引入了结构化绑定、if constexpr、std::optional、std::variant、std::string_view、并行算法等特性。
- C++20 是继 C++11 之后又一个重大更新,引入了概念(concepts)、范围库(ranges)、协程(coroutines)、模块(modules)、三元运算符的改进、constexpr 的增强、std::span 等新特性。
- C++23 是 2023 年 7 月份刚确定下的新标准,目前能完整支持 C++23 的编译器基本没有。变化包括引入标准库的模块化支持、扩展 constexpr 、增加并行算法、ranges 扩展、this 推导、引入更多的属性和注解、增加 std::mdspan、std::generator 等新特性。