工作上写C++,从去年底开始关注Rust,至今用Rust写了一些和交易相关的小程序。总体感觉是如果有一门语言能够取代C++,那么它只可能是Rust。
为什么这么说呢?首先我们来说一下
为什么很多情况下人们会选择C++。
很多人用C++的不是因为C++有多好,只是因为如果想要写一个接近实时高性能,稳健,并有足够开发效率的大程序,通常只有C++可选。这有如下几个原因:
-
要写接近实时,就不要能有垃圾回收(GC)。GC对于一些类型的程序几乎是致命的。想像一个低延时交易程序在看到一个交易机会之后因为某种原因触发长达100ms的GC,这个机会铁定就没了。要想达到接近实时且高性能就需要接近底层,即所谓bare-metal。
-
高性能可以用C达到,但考虑足够的开发效率,C的语言特性缺乏就是一个明显问题。我们也不想在组一个小组开始开发的时候还要来写一遍数据结构基础设施。C++的多语言范式提供了这样一些基础的组件,多数情况下你不需要再去写一个hashmap或者查找算法。
-
从开发效率和可读可维护性上来说,足够的抽象能力是必须的,但这种抽象必须是没有运行时开销的(runtime cost)。零开销抽象(zero cost abstraction)是C++的设计原则之一。inline函数,constexpr程序,template,都是遵循这一原则,编译器如果发现虚类(virtual class)没有真正被用到甚至会优化掉虚表(virtual table)。
-
稳健的大程序则意味着程序要尽量把错误消灭在进生产环境之前,要达到这个目标,我可以牺牲一点开发效率。这就意味着我需要一个静态类型最好同时是强类型的语言,使得编译器能够早发现问题。会强调一些诸如单参数构造函数最好加explicit之类的"技巧"来加强类型检查。C++11在这个基础上加了如enum class。各种override,final等都是加强编译器在编译时查出错误的能力。
总体上来说,就是一个高性能的静态强类型多范式语言。
但同时,C++问题很多,这些问题不是语言设计者能力不够,而多数是历史原因与当时抉择所看重的东西导致的。C++一开始的时候一个目标是与C兼容,即使C当年算是极其天才的设计(和Unix一样),它毕竟是1972年的语言,当时设计导致的问题虽然我们现在已经很清楚,C++也因为兼容性而把它保留了下来。大家都知道Python 2和Python 3的故事,甚至IA64和x86_64的故事,所以也很容易理解C++为什么要一直保持向前兼容。
Rust由于是一个新语言,所以它完全没有历史包袱。甚至在今年之前的五年中,社区一直完全肆无忌惮的做和之前完全不兼容的改动。如果发现有个新设计确实公认更好,语言作者们不会为了向前兼容而放弃改变。近30年的语言理论研究和实际软件工程的经验有足够多的优秀设计可以直接拿来使用。Rust作为一个新语言显然不会放过这个机会。在可以完全重来的前提下,
Rust相对C++有哪些新特性呢?
-
无data-race的并发。使用类似golang的channel-based并发非常易用。在不用lockfree的情况下,开发和运行效率都足够高,逻辑简单,加上下面的杀手特性避免data-race。
-
函数式语言的pattern matching。网站就提供了一个简单的例子,用惯指令式和面向对象的朋友可能需要一段时间发挥它的强大之处。实际上Rust还提供非常多的函数式语言特性,包括强大的closure,由于下面要提到的杀手级特性的保证,Rust的closure十分安全。
-
Generics和Trait粗看起来是zero cost abstraction的编译时多态(compile-time polymorphism),类似于C++中的template和C++17里的Concept。但实际上它设计的精巧已经远不是C++中template的同类了。其中一点就是统一了compile-time和run-time polymorphism,编译时多态叫trait,运行时多态叫trait object,省去了不少程序语义方面的复杂性。Trait这个特性这也是很多把golang看成是静态类型系统语言而又发现它竟然没有编译时多态的同学震惊而无奈的离它而去的原因。(golang作为服务器语言仍然是相当不错的选择,不过现在已经没有多少人还把它看成是系统语言了。)
-
灵活的enum系统,以及衍生的错误处理机制。Rust没有exception,错误是通过enum返回的。与golang在正确时也会返回错误代码不同,Rust错误代码只有在错误时才返回。这不仅仅只是方便,而有更重要的好处。
-
简单易用的与C语言的互通性。因为没有runtime,与C语言的互通简单是很自然的。这篇文章讲得很详细。
-
灵活的module系统。module系统以及pub和use关键字让你灵活的控制所有的访问许可关系。
-
强大的管理系统Cargo和中心化的库管理。Cargo的依赖管理遵循最新的。只需要适当选择库的依赖版本,一个cargo update会自动完成所有版本匹配和下载加载。被C++依赖摧残的小朋友们可以开心了。
-
其它的诸如,缺省可使用的直接打印数据的内容状态,更好用的宏,更漂亮的条件编译,语言自带的简单测试系统,rustdoc的自动文档生成,都让开发的效率和快感大增。一些小的改动,如缺省是不可变而不是mut的声明,match的exhaustive checking,都使人更容易写出更正确的代码。
当然,如果只有这些小改进,C++程序员可能看了看觉得,“不错哟”,然后就接者回去写C++了(比如D)。新语言必须有
杀手级的特性,这才是转语言的关键动力。
这一杀手级的特性就是Ownership和Lifetime。Rust首页宣称的"prevents nearly all segfaults, and guarantees thread safety"是超级诱人的,因为这是在没有GC和runtime的情况下实现的。
C语言的野指针,和线程安全问题会导致很多极难发现,诊断和修复的bug。C++,尤其是C++11费心思去缓和了这一问题,但这仍然是没有GC讲求性能语言的核心劣势。Rust这个特性具体是如何的呢?
-
Rust里每一个引用和指针都有一个lifetime,对象则只不允许在同一时间有两个和两个以上的可变引用。出了lifetime编译器在编译时就会静止它被使用。这样,如果一个操作使另一个引用不再可用,编译器就会发现它。C++里面的使用to_string().c_str()导致crash的bug应该不止一个人见过,这种bug在rust里是不会被编译通过的。
-
这种特性使得原来在C++里不敢使用的一些优化做法变得完全可能。比如把一个c_string分割成不同分,并在不同的地方使用。如果项目较大,为了完全起见,通常我们会把分割的结果拷贝一份单独处理,这样不需要害怕处理的时候那个c_string已经不存在。而使用Rust我们可以不用拷贝,而直接使用原来的c_string而不用担心野指针,lifetime设定可以让compiler去做这一繁琐的检查,如果有任何的c_string在处理分割结果之前被使用,编译器会告诉你。
这一特性所导致的编程可以衍生很多新的优化可能,而这都是在保证完全的前提下。实际上,催生Rust的浏览器Servo项目一个目标就是安全问题,Rust在安全性让heartbleed问题出现的可能大大减小。
最后,Rust是一个脚踏实地(Practical)的语言。这意味着它不遵循某一个范式(paradigm)或者是为计算机科学教学而生的语言。这是一个写给开发者的语言,如果特性经过权衡发现对开发有益,语言作者不会因为它破坏了某种范式而不去加它。这种哲学有些类似于当年Linux相对Mimix选择monolithic kernel而不是结构上更干净的micro kernel,因为Linux需要性能。这样的选择会让语言更接近使用者,让使用者更开心。
所以用C++的那些人的那些要求,Rust都能达到或者甚至改善。
-
无GC实时控制,接近底层没有overhead。在达到同样安全性的情况下,Rust不会比C++慢。
-
足够多的语言特性保证开发效率,比C++吸收了更多的现代优秀语言特性。
-
与C++一致的Zero cost abstraction
-
杀手级的ownership和lifetime,加上现代语言的类型系统,这是比C++强最多的地方。
在语言层面上Rust无疑比C++优秀得多的一个高性能静态强类型多范式语言。
如
Rust 1.0 Announcement所说,1.0标志着Rust已经稳定。你能在1.0编译器上使用的特性都会保证继续存在,除非重大正确性(soundness)问题出现。现在已经没有“下一个版本出来,之前的代码就不能用”的顾虑了。
1.0的发布是Rust发展的一个动力。当然真正长时间的采纳率还要看语言本身质量和社区环境。对这两点我是相当看好的。要想取代C++绝不是一件容易和快速的事,这不紧只是语言的问题,而有很多不可控的其他因素,让我们拭目以待吧。