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

2023-11-18


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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<T>、Result<T,E>、Some<T>、None、Ok<T>、Err<E>等出镜率也是很高的,为程序员表达自己的设计意图提供了强力的工具。

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

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

Rust似乎吸取了这方面的教训,并没有提供异常机制,而是通过上述提到的Result<T,E>、Option<T>、模式匹配(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<T>
  • 指示编译器的属性,如让结构体支持整体打印,可在结构体定义处加上#[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,香! - 知乎 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++程序员视角下的Rust语言 的相关文章

随机推荐

  • VS2019中文输出乱码解决方法(C语言)

    现象 VS2019控制台输出中文乱码 第一种解决方法 安装插件Format on Save重启VS2019生效 注意 别装错了 刚开始我就装错了这个UTF 8 No BOM 装了这个插件的同学 记得要删掉 不然还是会出现问题 第二种解决方法
  • 等价类

    动态测试方法是指通过运行被测程序 检查运行结果与预期结果的差异 并分析运行效率 正确性和健壮性等性能 这种方法由三部分组成 构造测试用例 执行程序 分析程序的输出结果 静态方法是指不运行被测程序本身 仅通过分析或检查源程序的语法 结构 过程
  • 无线通信原理之OFDM技术

    补充一个完整的OFDM系统结构图 包括收发天线 目录 1 OFDM的基本原理 2 OFDM系统模型 3 循环前缀和导频 4 OFDM系统参数 1 OFDM的基本原理 OFDM即正交频分复用 Orthogonal Frequency Divi
  • React-错误边界与组件通信方式概述

    错误边界 错误边界 Error boundary 用来捕获后代组件错误 渲染出备用页面 注意 只在生产环境 项目上线 起效 特点 只能捕获后代组件生命周期产生的错误 不能捕获自己组件产生的错误和其他组件在合成事件 定时器中产生的错误 简单理
  • DevOps是什么

    DevOps 英文Development和Operations的组合 是一组过程 方法与系统的统称 用于促进开发 应用程序 软件工程 技术运营和质量保障 QA 部门之间的沟通 协作与整合 它的出现是由于软件行业日益清晰地认识到 为了按时交付
  • JavaBean SpringBean是对象还是类

    JavaBean SpringBean是对象还是类 什么是JavaBean 什么是SpringBean 首先先说结论 Bean可以理解为对象 这几天在学习Spring源码的时候 观察到底层反复的对Bean的操作 于是就去网上搜索Bean到底
  • JAVA小程序微信支付

    微信支付有专门的文档 https pay weixin qq com wiki doc api wxa wxa api php chapter 9 1 当时找的时候都是前台如何 后来才发现后台需要做的就是统一下单 一 先到微信下载两个证书
  • java实现冒泡排序,直接插入排序,选择排序,希尔排序,以及求出它们的时间复杂度O(n)

    package com yg sort author GeQiLin date 2020 2 25 16 53 import java util Arrays public class Sort1 private static int ma
  • ModuleNotFoundError: No module named 'tqdm'

    bogon faceswap master macname pip3 install tqdm Collecting tqdm Downloading https files pythonhosted org packages 9f 3d
  • 软件版本(release、stable、lastest)的区别

    snapshot 快照 也即开发版 我们创建maven项目时 编辑器会自动给我们填入 1 0 SNAPSHOT版本 也就是1 0开发版 这个版本不能使用 因为该版本处于开发的过程 所以运行时会不时地更新 导致功能变化 正式环境中不得使用sn
  • layer.js open 隐藏滚动条

    img echart trand click function var host this data host var role this data role console log host host console log window
  • 华为防火墙NAT

    目录 NAT分类 黑洞路由 Server map 表 NAT分类 在内外网的边界 流量有出 入两个方向 所以NAT技术包含源地址转换和目的地址转换 一般情况下 源地址转换主要解决内部局域网计算机访问internet的场景 而目标地址转换主要
  • Qt/C++项目架构经验总结

    一 通用规则 除了极小的微型demo级别项目外 其余项目建议用pri分门别类不同文件夹存放代码文件 方便统一管理和查找 同类型功能的类建议统一放在一起 如果该目录下代码文件数量过多 也建议拆分多个目录存放 比如就3 5个界面的项目 统一搞个
  • Android 编码规范

    文章目录 背景 制定规范 一 开发环境 二 命名规范 1 资源命名 2 包命名 3 方法命名 4 类命名 5 变量 6 常量 二 编码规范 三 注释规范 1 类注释 2 方法注释 2 变量注释 3 区块注释 四 代码提交规范 五 其他须遵循
  • 快乐的强化学习4——Policy Gradients及其实现方法

    快乐的强化学习4 Policy Gradients及其实现方法 学习前言 简介 举例应用 神经网络的构建 动作的选择 神经网络的学习 具体实现代码 学习前言 刚刚从大学毕业 近来闲来无事 开始了机器学习的旅程 深度学习是机器学习的重要一环
  • [论文解读] 多机器人系统动态任务分配综述

    https www emerald com insight content doi 10 1108 IR 04 2020 0073 full html 多机器人 多智能体 动态环境 任务分配 决策 动态任务调度策略 该文章主要是想对目前st
  • jmeter+maven

    1 创建maven Project jmeter test 2 文件 1 src test目录下创建jmeter文件 2 src test jmeter目录下创建xx项目名文件 3 xxx jmx 脚本文件及依赖文件 放在src test
  • ajax post微信接口,微信小程序的ajax数据请求wx.request介绍

    微信小程序的ajax数据请求wx request介绍 微信小程序的ajax数据请求 很多同学找不到api在哪个位置 这里单独把小程序的ajax请求给列出来 微信小程序的请求就是wx request这个api wx request 一些对象参
  • 语音增强技术

    我的书 淘宝购买链接 当当购买链接 京东购买链接 在人机交互的场景中 麦克风采集到的人声不可避免的混杂外界噪声 可以通过信号处理的方法在处理前将其去除 以获得 纯净 的语音信号 这一过程称为降噪或者语音增强 从麦克风数量上分为单麦克降噪 多
  • C++程序员视角下的Rust语言

    自上世纪80年代初问世以来 C 就是一门非常重要的系统级编程语言 到目前为止 仍然在很多注重性能 实时性 偏硬件等领域发挥着重要的作用 C 和C一样 可以通过指针直接操作内存 这给了程序员的编程提供了相当大的自由度 但指针就是一把双刃剑 给