曾经的 C++ 现在的 Rust,吗?

引言

近些年来,越来越多的程序员都开始谈论 Rust 这门语言,虽然我很早就有关注,但一直没有时间或不愿意花时间去学习。前一周因为工作需要才对其进行了几天系统的学习,同时把 rustlings(学习 Rust 的练习题)给完成了。

大家都说,C++ 程序员转 Rust 语言是一件很容易的事情,因为 Rust 里最麻烦的内存管理机制,在 C++ 这门语言中早已经学习(被坑)过了,而对于其他语言的使用者(特别是动态语言)来说倒是新鲜事情,需要从头学习。在我看来,C++ 程序员转 Rust 最主要还是因为没有动力,而且 C++ 程序员对于 C++ 这门语言付出了太多的精力,继而不愿意去面对这种沉没成本,虽然大部分精力都只是为了避免 C++ 语言本身的坑而已。

并不严格的编译器

在 C++ 语言中,有很多的问题是因为这个语言本身的缺陷导致的,它为了兼容 C 以及老版本的 C++ 而做出了各种妥协。例如,C++ 中的移动语义的问题。当一个变量被移动后,按照 C++ 标准,该变量处于 valid but unspecified state(有效却未知的状态)。此时,使用者得非常小心,不要随意去使用被移动过后的变量,否则会出现未定义的结果。同时,C++ 编译器对此类的行为完全不在意,甚至都没有警告,只有开发者自己肉眼去检查代码是否存在这样的问题。而在 Rust 中,当一个变量被移动后,后续代码尝试使用这个被移动了的变量将会直接导致编译器报错,直接从编译层面上杜绝了出现这种未定义结果的可能性。

std::string a{"hello world"}, b;
b = std::move(a);
// 这时变量 a 在 C++ 中处于不确定的状态,不可以直接使用,而需要将其赋值后才能使用。但是若此时使用变量 a,却能够通过编译器的检查。

还有从 C 语言中继承过来的指针,也是各种问题的根源,这个也不需要在此赘述,太多太多的文章和相关讨论。

强大却随意的模版 template

C++ 里涉及模板的编译错误也是非常让人头疼。当你带入的模板参数有问题时,由于 C++ 语言特性,其没有类似 Rust 中的 Traits(特性说明),导致编译器无法找出正确的错误代码的位置。所以 C++ 编译器只能将整套模板推演的步骤列出来,往往一个参数类型带入错误会显示一千多行的带着模板推演的编译错误,最终你读完了这一千多行错误信息,终于在错误信息中找到了与你写的那行导致出错的代码相关的信息。这就是:好吧,我来猜猜看哪里错了,我试着改一下看能编译通过了没?事实上 C++ 标准委员会早就已经意识到这个问题,当时有一个提案是增加一个 Concept(概念)的关键字,其就类似 Rust 中的 Traits,用于约束和检查模板参数。但是已经十年过去了,不知道在新版本的 C++ 20 里有增加进去了没有,反正在 C++ 17 的版本中,仍然没有 concept 关键字。

这还不用说针对模版编程中的各种特化和偏特化,在编译时可针对带入的模版参数特性,由不同的模版代码来进行代码实体化。这其实是宏的一种衍生用法,但是却参杂着各种模糊的表述方法。就象是你在一个充满了各种江湖黑话的码头与一个拿着刀的黑帮老大约定只能用眼神来谈生意,一个错误的眨眼动作,你的左手无名指就离开了你,然后独自踏上去西天的取经之路,而此时的你还在捧着黑话手册逐页阅读。

虽然包管理可以用 CMake + vcpkg

还有 C++ 的包管理系统也是让人诟病。你说要写一个项目,哪里能够没有编译脚本和扩展包管理呢,我不可能所有的东西都自己写,造轮子虽然是 C++ 程序员的一生追求,但工期有时候是真的紧张。还好随着 CMake 逐渐崛起,其已经成为了事实上的 C++ 编译脚本语言。但是 CMake 自身也由于其历史也存在着大量的问题。具体来说,CMake 的语法不够直观,学习曲线较陡;它的错误信息往往不够清晰,导致调试困难;对于大型项目,CMake 的构建速度可能会变得很慢。此外,CMake 虽然能够管理依赖,但只能通过手动下载并编译网上的源代码包来进行依赖管理,这些功能相对基础,无法与现代语言的包管理器相比。近年来,一些真正的 C++ 包管理工具如 Conan 和 vcpkg 开始崭露头角,试图解决这些问题。它们提供了更现代的包管理体验,包括版本控制、依赖解析等功能。然而,由于 C++ 生态系统的分散性和历史包袱,这些工具还未能完全统一 C++ 的包管理方案。

强大却在臃肿的道路上老去的 C++

C++ 就是一门产生于早期计算机工程学尚未完备时的语言,它经过多次修补后终于成为了一个臃肿巨大的怪胎。确切地说,它其实只是一个实验品,但却又具有必然性。可悲的是,在当今的计算机领域中,至少 10%-20% 的代码都由 C++ 编写,而且很多还是很多软件的基础库构建基于 C++。这种情况的形成有其历史原因。C++ 诞生于 20 世纪 80 年代,当时计算机科学还处于快速发展阶段,许多现代编程概念尚未成熟。C++ 的设计初衷是为了在 C 语言的基础上增加面向对象编程的功能,同时保持对 C 的兼容性。这种设计理念导致了 C++ 复杂的语法和众多的特性。

对于 C++ 的问题修正和升级,C++ 标准委员会像一个被绑着手脚的人,要照顾 C++ 对 C 语言的兼容,要照顾 C++ 对自身的兼容,任何一项改动都会让 C++ 这个臃肿的语言变得更加凌乱不堪,再不用说现在已经存在的大量的 C++ 代码库,改变意味着其会被迅速因版本而割裂,而对这些老版本的代码库的维护和升级,又是一笔巨大的投入。但 C++ 语言本身,却又太过自由,程序员想写出什么样的问题代码都可以,各种指针越界,各种未定义行为,只有想不到,没有做不到。而在 Rust 这门语言里,编译器超级严格,当你的代码存在着不明确的行为时,你需要将该行为定义清楚,特别是对于生命周期的规定,从根本上杜绝了大部分未定义的行为的产生。

随着时间的推移,C++ 不断增加新的功能以适应不断变化的编程需求。每一次标准的更新都会引入新的特性,如 C++11 的移动语义和 lambda 表达式,C++14 的泛型 lambda 和变量模板,C++17 的结构化绑定和 if constexpr,以及 C++20 的协程和概念等。这些新特性虽然增强了语言的表达能力,但也使得语言变得越来越复杂。然而,随着新一代系统编程语言如 Rust 的出现,C++ 的一些固有问题正在被重新审视。这些新语言吸取了 C++ 的教训,提供了更安全、更现代的编程范式,同时保持了高性能。尽管如此,C++ 仍在不断发展,试图解决其存在的问题。C++20 和即将到来的 C++23 标准都引入了一些重要的改进,旨在使语言更加安全和易用。

Rust 来了

它最早是由 Graydon Hoare 在 2006 年发明并开始开发的语言,Hoare 后来加入了 Mozilla,而 Mozilla 于 2009 年开始赞助该项目,该项目于 2010 年公开,其第一个版本于 2012 年 1 月发布。2021 年,由 Google、Microsoft、AWS、Huawei 及 Mozilla 共同成立了基金会,并且承诺在接下来的两年时间里,每年投入不少于 100 万美元的预算,用于 Rust 项目的开发、维护和推广。

Rust 对于 C++ 的问题进行了改进。具体来说,Rust 在以下几个方面显著优于 C++:内存安全方面,Rust 的所有权系统和借用检查器在编译时就能捕获大多数内存错误,如空指针引用、数据竞争等,大大减少了运行时错误和安全漏洞;并发编程方面,Rust 的类型系统和所有权模型使得并发编程变得更加安全和容易,能在编译时防止数据竞争,这些在 C++ 中实现起来会比较复杂;包管理方面,Rust 有一个官方的包管理器 Cargo,它简化了依赖管理和构建过程;错误处理方面,Rust 的 Result 和 Option 类型,以及 ? 运算符,使得错误处理变得更加优雅和健壮;零成本抽象方面,Rust 提供了高级语言特性,如泛型和 trait,同时保持了与 C++ 相当的性能;语法方面,Rust 的语法设计更加现代和一致,避免了 C++ 中的许多历史遗留问题;工具链方面,Rust 提供了更现代、更集成的工具链,包括 rustfmt 用于代码格式化,clippy 用于静态分析等。这些改进使得 Rust 成为一个更安全、更现代的系统编程语言,同时保持了高性能和低级控制的特性。

Rust 是一门现代化的系统编程语言,它在设计之初就吸取了 C++ 等前辈语言的经验教训。Rust 不仅继承了 C++ 的高性能和底层控制能力,还通过创新的语言特性解决了许多 C++ 长期存在的问题。它的内存安全机制、并发编程模型和强大的类型系统等特性,使得开发者可以更加高效和安全地编写系统级软件。虽然 Rust 相对年轻,但它已经在系统编程、Web 开发、嵌入式系统等多个领域展现出巨大的潜力,为开发者提供了一个兼具安全性、性能和生产力的编程环境。

随着 Rust 的不断发展和成熟,它在各个领域都展现出了巨大的潜力。除了传统的系统编程领域,Rust 在 Web 开发、网络编程、游戏开发等方面也有着广泛的应用。特别值得一提的是,在区块链和加密货币领域,Rust 正在成为一个越来越受欢迎的选择。Rust 的安全性、并发性和性能优势使其成为区块链开发的理想选择。许多新兴的区块链项目,如 Solana、Polkadot 和 Near Protocol,都选择了 Rust 作为其主要开发语言。这些项目需要处理复杂的并发操作、高性能计算和严格的安全要求,而 Rust 恰好能够满足这些需求。

但是 Rust 也不是完美和没有缺点,它对于安全性的强调,让程序员在写代码的时候需要提供更多的描述信息来确保安全。这种严格的安全机制虽然能够有效地防止许多常见的编程错误,但也带来了一些挑战。首先,Rust 的学习曲线较为陡峭,特别是对于那些习惯了其他编程语言的开发者来说。所有权系统、生命周期和借用检查器等概念可能需要一段时间才能完全掌握。其次,Rust 的编译时间通常比其他语言长,这可能会影响开发效率,尤其是在大型项目中。此外,Rust 的生态系统虽然在不断发展,但相比于一些更成熟的语言,仍然存在一些库和工具的缺失。

Rust 没有像 C++ 那么完善的 IDE。虽然 Rust 的 IDE 支持在近年来有了显著改善,但与 C++ 相比仍有一定差距。主流的 IDE 如 Visual Studio Code 和 IntelliJ Rust Rover 都支持语法高亮、代码补全和基本的重构功能,但是都存在对项目代码分析速度过慢的问题。Rust Rover 其实要比 Visual Studio Code 好不少,是因为其在 rust-analyzer 的基础上对代码分析的工作做了改进,而 Visual Studio Code 只使用了 rust-analyzer 来做代码分析基础工具。而且 rust-analyzer 不写 cache 文件,这意味着,每次打开项目,对于该项目的分析要全部从头来一遍,而稍大一些的项目分析一次都需要半个多小时甚至一个小时,有时候真是不可接受。对于这个问题,使用 Visual Studio Code 或其它基于 rust-analyzer 为分析基础的 IDE 的唯一的解决办法就是,不要关闭你的 IDE。或者使用 Rust Rover 来开发 Rust,不过因为 Rust Rover 并不是开源的产品,所以若你有开源洁癖的话,那就是另一个故事了。

当然我一直使用 NeoVim,在 Tmux 里打开一个 NeoVim 通过 Rust-analyzer 载入并分析好了 Rust 工程里的所有代码后基本上就不再关闭了。