热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

C++程序员视角下的Rust语言

自上世纪80年代初问世以来,C就是一门非常重要的系统级编程语言。到目前为止,仍然在很多注重性能、实时性、偏硬件等领域发挥着重要的作用。C和C一样&#x


自上世纪80年代初问世以来,C++就是一门非常重要的系统级编程语言。到目前为止,仍然在很多注重性能、实时性、偏硬件等领域发挥着重要的作用。

C++和C一样,可以通过指针直接操作内存,这给了程序员的编程提供了相当大的自由度。但指针就是一把双刃剑,给了程序员灵活、自由地表达设计意图的同时,也给程序埋下了非常大的隐患。

C/C++程序员一定被各种程序崩溃、线程死锁、程序行为不正确等问题折磨过,要知道这些都是程序中最严重的Bug了。 这些问题有时还非常灵异,有时很快就可以复现问题,有时很久都难以出现一次。但只要程序哪怕出现过一次这样的问题,也就说明了自己的程序是存在漏洞和隐患的。

程序员肯定是非常害怕这样的程序被发布并部署到实际生产环境中的,这无疑给自己埋了一个雷啊。如果这是非常重要的软件(如火箭飞行控制程序、银行交易系统等),那么一旦问题发生,后果是难以预料的。

为了避免此类问题的发生,C/C++程序员通常需要付出更多时间、精力、耐心去学习更底层的计算机系统的工作原理和技术细节;在软件设计和编程时,还需要非常地细致和小心,反复琢磨自己的程序是否存在低级错误和逻辑漏洞等。

但有时候,即使程序员非常小心,错误还是难以避免的。因为C和C++根本没有提供一种对这类问题的检查和保证机制,而完全相信程序员自己去解决这类问题。 实践证明,这根本是靠不住的。这对编写程序的C/C++程序员有着非常高的要求,即使经验非常丰富的C/C++程序员,也不一定敢保证自己的程序完全不会存在这类问题吧。

既然人不一定靠的住,那么就要在机器层面提供强力的检查和保证机制去规范程序员遵守一定的规则以避免这类问题的发生。

针对内存安全问题,目前大部分的高级编程语言基本都是内置垃圾回收机制。这的确避免了不安全的内存操作,减少了程序员的心智负担,但这是通过性能的代价换来的,也就注定了这类编程语言的使用范围是受限的,对硬件的性能是有一定要求的。

C++很早就已经意识到这样的问题,也提出了自己的方案,即基于RAII机制的智能指针,没有走内置垃圾回收机制的路子。

RAII机制是C++问世以来就一直存在的,本质上是C++对象的确定性析构机制,即对象在其生命周期结束时,析构函数保证会被自动调用。

传统的裸指针无法承载资源所有权机制和生命周期管理的实现,但增加一层抽象的智能指针则可以,核心思想是用栈上对象管理堆内存/内核资源:

  • std::unique_ptr表达了独占所有权机制,无论什么时候都只有唯一的对象持有资源的所有权,但所有权可以通过移动语义转让的
  • std::shared_ptr表达了共享所有权机制,通过引用计数机制,保证可以有多个对象同时持有某个资源的所有权,只有在引用计数为0时,资源才会被释放
  • std::weak_ptr则完全是配合std::shared_ptr而存在的,不影响所有权,但却有方法可以知道自己手上的资源是否还存活/有效,这是裸指针做不到的

如果说C++在内存安全上做出了自己的努力,那么在线程并发安全上则努力程度还不够,这部分基本还是需要靠程序员自己去保证的。

而Rust则是从一开始就在内存安全和线程安全上下足了功夫,同时没有抛弃性能。Rust自始至终给自己的定位就是一门系统级编程语言。

Rust和C++一样,没有走内置垃圾回收机制的路子,而是从语言的内在机制上去解决C和C++内存安全和线程安全的痛点。

Rust通过更强大和完善的类型系统和所有权机制,引入了如下核心语言内在机制:

  • 值的唯一所有权
  • 默认内置基本类型的值拷贝语义
  • 默认对象的移动语义(所有权转移)
  • 默认不可变(只读)内存访问
  • 所有权不可变引用
  • 唯一所有权可变引用
  • 跨函数引用的生命周期标注
  • 不支持空指针

结合Rust核心库和标准库提供的Send和Sync trait、智能指针、Option等,保证程序的内存安全和线程安全。需要注意的是,Rust对于线程安全,只能做到避免数据竞争,无法做到避免条件竞争;另外,在Rust中,把引用更习惯称为借用(borrow),以强调借用所有权之意。

Rust标准库提供了几种与C++类似的智能指针:

  • Box,相当于C++中的std::unique_ptr
  • Rc,相当于C++中的std::shared_ptr
  • Weak,相当于C++中的std::weak_ptr
  • Arc,线程安全的Rc
  • ......

Rust的类型系统在很大程度上借鉴了Haskell语言的类型系统,而在内存安全的所有权机制上则是充分吸收C++的RAII机制思想。

Rust和C++一样,也是支持面向对象、泛型编程、函数式编程等多种编程风格/范式的编程语言。

从面向对象编程的角度来看,Rust和C++在对象概念的语言表达形式上存在明显的不同。

对C++程序员来说,类的概念是深入人心,构造函数和析构函数不可或缺。但Rust是没有类的概念的,对等的概念则是强化到了结构体(struct)上了,可能是认为C++中class和struct是差不多的,只是默认访问权限上不同。

在Rust中,struct的定义则纯粹表达了C++中类的数据成员部分,而完全不会看到任何函数的影子,当然数据成员的访问权限默认也是私有的; C++类的成员函数,在Rust中是在impl块中单独进行描述的。

但和C++类最大的不同,感觉还在构造函数和析构函数上。在Rust中,是不支持构造函数的,而析构函数则是需要通过实现Drop这个trait来表达的。Rust的惯例是在struct的impl块中实现一个New函数来模拟C++的构造函数。当然,这个函数的调用是需要程序员自己去手动调的,Rust编译器不会有任何额外的支持;而实现Drop的struct,Rust编译器则会保证在其对象的生命周期结束后,drop函数一定会被自动调用。这样才能保证实现RAII机制。

Rust中的类成员函数和类静态成员函数的区别在于第一个参数是不是&self或&mut self,有则表示是类的成员函数,而没有则是类的静态成员函数。在Rust中,self相对于C++中的this,区别在于C++类成员函数的this是不需要程序员自己写出来的,由编译器生成,而在Rust中则需要程序员自己写出来。而使用&self的成员函数,则相当于C++中const成员函数;使用&mut self,则是C++中非const的成员函数。

Rust中trait是非常重要的概念,它承担了类似C++中通过纯虚类表达接口的意图。Rust中强调组合优先继承的思想,不支持struct级的继承,但支持trait的接口继承,这和Java等编程语言一样。

和C++中虚函数类似,Rust中trait中负责定义接口函数的原型,也可以为接口函数提供默认的实现。特别的是,Rust也支持不提供任何接口的trait,这样的trait则退化为标签的概念。在Rust中,作为标签使用的trait很常见,例如核心库中提供的Copy、Send、Sync等trait就是这样,主要用于给Rust编译器标识出某种语义,便于编译器进行相关的类型安全检查。

C++支持虚函数和继承表达的动态多态性和基于模板的静态多态性。Rust则做得更好,通过trait机制统一了动态多态性和静态多态性的表达形式,而且是一个实现可以同时支持这两种多态性。

Rust中,动态多态性的具体表达形式和C++是类似的,例如,通过将trait引用作为函数的形参,而给这个函数传实参时,必须要传实现了该trait的对象;而静态多态性也是通过泛型实现的,但在表达对泛指类型T的约束上要比C++完善,而C++20的concept才能做到类似的表达效果。

在支持函数式编程上,少不了lambda表达式的支持,当然,Rust的枚举(enum)也功不可没。

和C/C++中的枚举不同,Rust的枚举值可以关联不同数据类型的值或不关联值,结合match的模式匹配,表达能力大大增强。 这种表达能力完全替代了C/C++中switch & case。

当然,在模式匹配的支持上,Rust标准库提供的Option、Result、Some、None、Ok、Err等出镜率也是很高的,为程序员表达自己的设计意图提供了强力的工具。

C缺乏有效的错误处理机制,而C++提供的异常机制并没有得到程序员广泛的认可,至少在用不用异常的问题上,大家是犹豫的,甚至有些公司明确禁止异常的使用,如Google的C++编程规范中就明确提出过。

禁用C++的异常,可能是考虑到异常本身带来的代码膨胀、性能等问题,也可能是某种历史性因素。没有异常的C++,在错误表达上就退化到和C一样的水平上了,基本就是基于返回错误码。

Rust似乎吸取了这方面的教训,并没有提供异常机制,而是通过上述提到的Result、Option、模式匹配(match、if let、while let)、panic!、assert!等来提供一套错误处理机制。

在形式上,偏向返回错误码的风格,但提供的内涵又比C的错误码要强很多;在性能上,也没有出现C++异常带来的问题。这充分体现了零成本抽象的设计思想。

C/C++中的宏是通过预处理器负责处理的,对编译器而言完全无感知。这就说明宏不属于类型系统的一部分,编译器无法对此进行安全检查。 正是这样,C/C++中的宏在使用时才要特别小心,否则,一不小心就会引入问题。在C++中通过增加一些额外的语言机制,让程序员去替代宏的那部分功能。 例如,提供内联函数机制替代宏函数、提供const去定义常量等。

而在Rust中,宏则被鼓励去使用,体现在Rust的标准库上就在广泛使用宏。 例如,println!、vec!等这些都是Rust标准库提供的宏。

Rust的宏,给了程序员一个可以自己去创造新语法的工具,这是有利于程序员写出更清晰明了的代码,提高代码的可读性。 而能写出面向人的代码则无疑是非常有价值的。 现代的高级编程语言,特别是动态编程语言,在这条路上越走越远。 越接近人的自然语言表达能力,程序员的生产效率就会越高。

现代的编程语言,对于程序的组织上,基本都抛弃了C/C++提供的头文件和源文件分离的机制。无疑,Rust也是这样。C++20提供的moudle机制也在走向这条道路。

在编程语言的互操作上,C的ABI无疑是一个事实上的标准。

Rust作为一个定位支持系统级编程的语言,肯定不会放弃和C的兼容性。这体现在Rust结构体的内存布局和基于trait实现的动态多态性上(在C++中,将虚函数表指针和结构体的数据成员放在一起,从而在对象内存布局上破坏了和C的兼容性)。

另外,为了充分利用现有C的代码,Rust提供了FFI机制和unsafe块。在unsafe块中可以绕过Rust严格的类型安全检查机制,而这部分的代码的安全性就自然需要程序员自己去保证了。

在一些基本的语言表达方式上,Rust和C/C++也存在一些不同,体现在:

  • 变量默认是不可变绑定(let),需要修改变量,则需明确使用可变绑定(let mut)
  • 没有实现Copy trait的对象,绑定、赋值、非引用传参时默认是移动语义
  • 支持函数内嵌定义
  • 支持函数表达式返回(最后不加分号)
  • 在同一个作用域内,变量可以重新绑定(let),在Rust中叫做遮蔽机制
  • 支持零尺寸的结构体、空枚举、空数组([T, 0])
  • 两种字符串类型变量:&str相当于C++中的const char*,用于指向字符串字面常量;而String相对于C++中的std::string,支持可变引用&Mut String和不可变引用&String
  • 基本的数据类型都实现了Copy trait,默认在栈上分配,支持复制语义;而String、Vec等默认只支持移动语义,要进行深拷贝,需要显式调用clone函数
  • 不支持switch & case,使用match模式匹配代替
  • 不支持三目运算符
  • 支持?运算符,用于调用的函数返回异常时,直接退出当前函数并返回对应的错误Err
  • 指示编译器的属性,如让结构体支持整体打印,可在结构体定义处加上#[derive(Debug)],相当于让编译器自动给指定的结构体加上实现Debug trait的代码
  • 支持文档化注释:///和//!,使用cargo doc可以基于代码生成对应的html文档;当然同时也支持C++的那两种形式
  • ......

Rust在对编程开发套件上的支持也是非常有吸引力的。

虽然目前Rust还没有自己的IDE,但强大的cargo和统一的包管理库(http://crate.io)为编程带来极大的方便,不用为搭建开发环境而费神了。

另外,Rust编译器的错误提示真的非常好,想起C++异常报错的天书,完全是两样的感受。

C++从C++11开始逐步走向现代化之路,而Rust则完全是一个现代化的编程语言。

虽然Rust定位于一门系统级编程语言,但它并没走C++兼容C的老路,完全没有历史的包袱,可以轻装上阵,充分吸收各家编程语言之长,避其之短。

Rust的设计目标是非常明确的,提供内存安全、线程安全而又不失性能的现代化系统级编程语言。

Rust有完全不亚于C++的表达能力和性能,又解决了C++的最大痛点(内存安全、线程安全),这对C++程序员来讲无疑是非常有吸引力的。

目前,C++仍然是我的主力编程语言,但我对Rust是看好的。

虽然现在对Rust的了解并不深入,只写过一些简单的Demo,并没有用于实际的开发,但我觉得Rust仍是值得C++程序员认真去学习的一门编程语言。

它不仅实用,反过来也会促进对C++中关键概念和问题的理解。


引用

如何看待 Rust 这门语言? - 知乎

连续 3 年最受欢迎:Rust,香! - 知乎 


推荐阅读
  • 本文介绍如何从字符串中移除大写、小写、特殊、数字和非数字字符,并提供了多种编程语言的实现示例。 ... [详细]
  • 深入解析Java虚拟机(JVM)架构与原理
    本文旨在为读者提供对Java虚拟机(JVM)的全面理解,涵盖其主要组成部分、工作原理及其在不同平台上的实现。通过详细探讨JVM的结构和内部机制,帮助开发者更好地掌握Java编程的核心技术。 ... [详细]
  • 深入理解Java字符串池机制
    本文详细解析了Java中的字符串池(String Pool)机制,探讨其工作原理、实现方式及其对性能的影响。通过具体的代码示例和分析,帮助读者更好地理解和应用这一重要特性。 ... [详细]
  • 本文详细介绍了优化DB2数据库性能的多种方法,涵盖统计信息更新、缓冲池调整、日志缓冲区配置、应用程序堆大小设置、排序堆参数调整、代理程序管理、锁机制优化、活动应用程序限制、页清除程序配置、I/O服务器数量设定以及编入组提交数调整等方面。通过这些技术手段,可以显著提升数据库的运行效率和响应速度。 ... [详细]
  • 在高并发需求的C++项目中,我们最初选择了JsonCpp进行JSON解析和序列化。然而,在处理大数据量时,JsonCpp频繁抛出异常,尤其是在多线程环境下问题更为突出。通过分析发现,旧版本的JsonCpp存在多线程安全性和性能瓶颈。经过评估,我们最终选择了RapidJSON作为替代方案,并实现了显著的性能提升。 ... [详细]
  • 版本控制工具——Git常用操作(下)
    本文由云+社区发表作者:工程师小熊摘要:上一集我们一起入门学习了git的基本概念和git常用的操作,包括提交和同步代码、使用分支、出现代码冲突的解决办法、紧急保存现场和恢复 ... [详细]
  • 在编译BSP包过程中,遇到了一个与 'gets' 函数相关的编译错误。该问题通常发生在较新的编译环境中,由于 'gets' 函数已被弃用并视为安全漏洞。本文将详细介绍如何通过修改源代码和配置文件来解决这一问题。 ... [详细]
  • Python 内存管理机制详解
    本文深入探讨了Python的内存管理机制,涵盖了垃圾回收、引用计数和内存池机制。通过具体示例和专业解释,帮助读者理解Python如何高效地管理和释放内存资源。 ... [详细]
  • 在进行QT交叉编译时,可能会遇到与目标架构不匹配的宏定义问题。例如,当为ARM或MIPS架构编译时,需要确保使用正确的宏(如QT_ARCH_ARM或QT_ARCH_MIPS),而不是默认的QT_ARCH_I386。本文将详细介绍如何正确配置编译环境以避免此类错误。 ... [详细]
  • 目录一、salt-job管理#job存放数据目录#缓存时间设置#Others二、returns模块配置job数据入库#配置returns返回值信息#mysql安全设置#创建模块相关 ... [详细]
  • Appium + Java 自动化测试中处理页面空白区域点击问题
    在进行移动应用自动化测试时,有时会遇到某些页面没有返回按钮,只能通过点击空白区域返回的情况。本文将探讨如何在Appium + Java环境中有效解决此类问题,并提供详细的解决方案。 ... [详细]
  • 2018-2019学年第六周《Java数据结构与算法》学习总结
    本文总结了2018-2019学年第六周在《Java数据结构与算法》课程中的学习内容,重点介绍了非线性数据结构——树的相关知识及其应用。 ... [详细]
  • 本文探讨了如何通过预处理器开关选择不同的类实现,并解决在特定情况下遇到的链接器错误。 ... [详细]
  • 本文深入探讨了面向切面编程(AOP)的概念及其在Spring框架中的应用。通过详细解释AOP的核心术语和实现机制,帮助读者理解如何利用AOP提高代码的可维护性和开发效率。 ... [详细]
  • 本文详细介绍了在不同操作系统中查找和设置网卡的方法,涵盖了Windows系统的具体步骤,并提供了关于网卡位置、无线网络设置及常见问题的解答。 ... [详细]
author-avatar
啊沙发的非飞
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有