“Rust思维下的C++编程”:在C++中,如何应用Rust中的概念?

C语言与CPP编程 2024-08-06 09:00

本文经授权转自公众号CSDN(ID:CSDNnews)

整理 | 郑丽媛

【编者按】自从美国白宫对开发者呼吁,“停止使用 C 和 C++,改用 Rust 等内存安全编程语言”后,两方之间从未停止的争论就被推到了一个新高度。而在这之中,也有部分 C++ 开发者提议:或许 Rust 中的一些概念,可以试着运用到 C++ 编程中?


近日,一位开发者(ID:delta242)在 Reddit 上发了一篇长文《在 C++ 中应用 Rust 的概念》,里面提到了一些可用于改善 C++ 代码的 Rust 概念,引来了诸多关注和讨论。

根据他在开篇的介绍,“虽然我不是 Rust 专家,但我很喜欢这门语言的许多概念。在日常编程中,我主要用 C++,而现在我经常会运用一些 Rust 的概念来改善我的 C++ 代码”,可以看出,下面是他亲身实践过的、可用于优化 C++ 代码的 Rust 概念。


1、在 C++ 中,如何应用 Rust 的概念?

(以下为他的长文翻译:)

(1)带值的枚举

我很喜欢 Rust 的枚举,因为你可以给枚举常量赋值(例如,Option 枚举中有一个没有值的 None 和一个有值的 Some)。在类型理论中,这通常被称为代数数据类型,而在 C++ 中,我们有 variants,可以定义辅助结构体来实现类似的功能:

struct Some { T value; };struct None { };using Optional = std::variant;

(注:这个例子可能有点蠢,因为 std::optional 要好得多。但对于更复杂的类型来说,这具有一定参考意义。)

(2)CRTP 和 Traits

在 Rust 中,Traits 用于定义类型的共享功能。而在 C++ 中,我们可以用 CRTP 在编译时强制类实现特定的函数来实现静态多态性。CRTP 还允许在基类中实现默认功能,我以前曾用这种方法来定义迭代器类型,只要基类实现了 operator[],就可以减少大量模板代码的编写。

(3)字符串格式化

在 C++ 中,如果向 std::format 传递的参数数量多于格式字符串中的占位符,并不会导致编译时错误。我曾经遇到过这样的 bug,例如由于缺少占位符,日志消息中缺少了某些信息,导致与代码中不一致。

而这个情况如果放在 Rust 中,就会产生编译时错误。所以这对于 C++ 来说,将是一个简单而实用的改进,有助于提高代码质量和开发效率。

(4)拥有 Mutex

在 Rust 中,Mutex 类型拥有受保护的值。我非常喜欢这个概念,这样不获取 Mutex 就无法访问受保护的值(这在 C++ 中经常发生)。有一个简单的技巧来实现类似效果,那就是在 C++ 中写一个具有 lock 函数的封装 Mutex 类,该函数将接受一个带有对受保护值的引用的 lambda 表达式作为参数。由于 Rust 中有借用检查器,这样的操作总是安全的,而在 C++ 中,误用很容易再次导致竞争条件,但至少通过这样的封装器,这种情况就不那么容易发生了。

(5)内部可变性

Rust 在安全的情况下会使用内部可变性(即使变量是 const),例如当一个值受 Mutex 保护时。在 C++ 中,我们也可以采用类似的想法,例如“const 表示线程安全”。

(6)IIFE

在 Rust 中,每个作用域都是一个表达式,这样可以很好地将变量限制在更小的作用域中。而在 C++ 中,我们可以用 lamdas 表达式来使用立即调用的函数表达式(IIFE)来达到同样的效果:

auto value = [] {  // Complex initializer  return result;}(); // notice the invocation

以上,就是我现在能想到的。


2、“Rust 让我成为了一名更好的 C++ 开发者”

在这篇长文下,不少开发者也分享了自己在 C++ 编程中借鉴 Rust 概念的心得,甚至直言“Rust 让我成为了一名更好的 C++ 开发者”。

(1)“最近,我养成了在 C++ 中使用“match”宏的这个习惯,我很喜欢。”

template struct overloaded : Ts... { using Ts::operator()...; };
template auto match(Val val, Ts... ts) { return std::visit(overloaded{ts...}, val);}

(2)“重载非常好,我觉得它可以成为 STL 的一部分。此外,有了 C++20 模板化的 lambdas,还可以编写一些非常花哨的代码。”

visit(  overloaded {    [] T>(T value) {}    [](auto other) {}  }, value)

对此,一位开发者感慨:“这正是我希望看到的,虽然我不喜欢 Rust,但它确实有一些 C++ 可以借鉴的做法,更安全总归是好的。”


3、在 C++ 中应用 Rust 概念的一些失败案例

不过与此同时,也有开发者提醒“必须小心”:以 Rust 的 Mutex 为例,当你访问 Mutex 中的数据时,不可能将该指针存储下来,然后在解锁 Mutex 后再访问数据(忽略特殊情况)。你可以在 C++ 中实现一个拥有 Mutex 的类,但编译器不会在意你是否在锁的作用域之外持有一个指向受保护数据的指针,并在未受保护的情况下访问它。

针对这个话题,开源搜索引擎 Meilisearch 的高级工程师 Louis Dureuil 曾写过一篇相关文章《这对 C++ 来说太危险了》:“一些设计模式之所以实用,归功于 Rust 的内存安全性,而在 C++ 中使用则过于危险。”

在文中,Louis Dureuil 分享了他在 C++ 中应用 Rust 概念的失败案例。

当时,他正在用 Rust 编写一个内部库,其中有一个他希望能克隆、而不会复制其中数据的错误类型。在 Rust 中,这需要使用引用计数指针,比如 Rc。他编写了一个错误类型,将其用作可能发生错误的函数的错误变体,继续了他的工作。

struct Error {    data: Rc,}
pub type Response = Result;
fn parse(input: Input) -> Response { todo!()}

后来他发现,对某些输入进行解析需要很长时间,于是决定通过通道将输入发送到另一个线程,并通过另一个通道获取响应,这样长时间的解析就不会阻塞主线程。

enum Command {    Input(Input),    Exit,}
pub enum RequestStatus { Completed(Response), Running,}
pub struct Parser { command_sender: Sender, response_receiver: Receiver<(Input, Response)>, cached_result: HashMap,}
impl Parser { pub fn new() -> Self { let (command_sender, command_receiver) = channel::(); let (response_sender, response_receiver) = channel::<(Input, Response)>();
std::thread::spawn(move || loop { match command_receiver.recv() { Ok(Command::Input(input)) => { let response = parse(input); let _ = response_sender.send((input, response)); } Ok(Command::Exit) => break, Err(_) => break, } });
Self { command_sender, response_receiver, cached_result: HashMap::default(), } }
pub fn request_parsing(&mut self, input: Input) -> RequestStatus { // pump previously received responses while let Ok((input, response)) = self.response.receiver.try_recv() { self.cached_result .insert(input, RequestStatus::Completed(response)); }
let response = match self.cached_result.entry(input) { Entry::Vacant(entry) => { self.command_sender .send(Command::Input(entry.key())) .unwrap(); entry.insert(RequestStatus::Running) } Entry::Occupied(entry) => entry.into_mut(), }; response.clone() }}

然而,在进行这一更改时,Louis Dureuil 收到了以下错误信息:

error[E0277]: `Rc` cannot be sent between threads safely   --> src/main.rs:58:32    |58  |               std::thread::spawn(move || loop {    |  _____________------------------_^    | |             |    | |             required by a bound introduced by this call59  | |                 match command_receiver.recv() {60  | |                     Ok(Command::Input(input)) => {61  | |                         let response = maybe_make(input);...   |68  | |                 }69  | |             });    | |_____________^ `Rc` cannot be sent between threads safely    |    = help: within `(&'static str, Result)`, the trait `Send` is not implemented for `Rc`note: required because it appears within the type `Error`   --> src/main.rs:17:16    |17  |     pub struct Error {    |                ^^^^^note: required because it appears within the type `Result`   --> /home/dureuill/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:502:10    |502 | pub enum Result {    |          ^^^^^^    = note: required because it appears within the type `(&str, Result)`    = note: required for `Sender<(&'static str, Result)>` to implement `Send`note: required because it's used within this closure   --> src/main.rs:58:32    |58  |             std::thread::spawn(move || loop {    |                                ^^^^^^^note: required by a bound in `spawn`   --> /home/dureuill/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/mod.rs:683:8    |680 | pub fn spawn(f: F) -> JoinHandle    |        ----- required by a bound in this function...683 |     F: Send + 'static,    |        ^^^^ required by this bound in `spawn`

正如编译器所解释的那样,这是因为 Rc 类型不支持在线程之间发送,因为这样会导致数据竞争。实际上,Rc 中的引用计数并不以原子方式进行操作,而是使用常规的整数操作。

为了实现线程安全的引用计数,Rust 提供了另一种类型 Arc,它使用原子引用计数,而将代码修改为使用 Arc 非常简单:

diff --git a/src/main.rs b/src/main.rsindex 04ec0d0..fd4b447 100644--- a/src/main.rs+++ b/src/main.rs@@ -3,9 +3,9 @@ use std::{io::Write, time::Duration}; mod parse {     use std::{         collections::{hash_map::Entry, HashMap},-        rc::Rc,         sync::{             mpsc::{channel, Receiver, Sender},+            Arc,         },         time::Duration,     };@@ -15,13 +15,13 @@ mod parse {
#[derive(Clone, Debug)] pub struct Error {- data: Rc,+ data: Arc, }
impl Error { fn new(data: ExpensiveToCloneDirectly) -> Self { Self {- data: Rc::new(data),+ data: Arc::new(data), } } }

也就是说,只要不需要引用原子操作的计数,就可以使用 Rc。但当需要线程安全时,编译器会强制 Louis Dureuil 切换到 Arc,并带来了原子引用计数的开销。

Louis Dureuil 指出,这个原则也深受 C++ 开发者的喜爱。但与 Rust 完全不同的是,在 C++ 中,标准库中只有带有原子引用计数的 shared_ptr,它相当于 Arc,而不是 Rc——所以,即使你不使用原子操作,也仍要为原子引用计数付出代价。

最后,一句话总结:在 C++ 中适当应用 Rust 概念固然不错,但切记不要根据在 Rust 中会发生的情况,对 C++ 也做出相同的假设。

参考链接:

https://www.reddit.com/r/cpp/comments/1bx7wjm/applying_concepts_from_rust_in_c/

https://blog.dureuill.net/articles/too-dangerous-cpp/

本文转自公众号“CSDN”,ID:CSDNnews
EOF

你好,我是飞宇,本硕均于某中流985 CS就读,先后于百度搜索字节跳动电商以及携程等部门担任Linux C/C++后端研发工程师。

最近跟朋友一起开发了一个新的网站:编程资源网,已经收录了不少资源(附赠下载地址),如果屏幕前的靓仔/女想要学习编程找不到合适资源的话,不妨来我们的网站看看,欢迎扫码下方二维码白嫖~

同时,我也是知乎博主@韩飞宇,日常分享C/C++、计算机学习经验、工作体会,欢迎点击此处查看我以前的学习笔记&经验&分享的资源。

我组建了一些社群一起交流,群里有大牛也有小白,如果你有意可以一起进群交流。

欢迎你添加我的微信,我拉你进技术交流群。此外,我也会经常在微信上分享一些计算机学习经验以及工作体验,还有一些内推机会

加个微信,打开另一扇窗

感谢你的分享,点赞,在看三  

C语言与CPP编程 C语言/C++开发,C语言/C++基础知识,C语言/C++学习路线,C语言/C++进阶,数据结构;算法;python;计算机基础等
评论
  • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
    一博科技 2025-01-21 16:17 95浏览
  •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
    电子知识打边炉 2025-01-22 11:12 42浏览
  •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
    刘旷 2025-01-21 11:15 261浏览
  • Ubuntu20.04默认情况下为root账号自动登录,本文介绍如何取消root账号自动登录,改为通过输入账号密码登录,使用触觉智能EVB3568鸿蒙开发板演示,搭载瑞芯微RK3568,四核A55处理器,主频2.0Ghz,1T算力NPU;支持OpenHarmony5.0及Linux、Android等操作系统,接口丰富,开发评估快人一步!添加新账号1、使用adduser命令来添加新用户,用户名以industio为例,系统会提示设置密码以及其他信息,您可以根据需要填写或跳过,命令如下:root@id
    Industio_触觉智能 2025-01-17 14:14 118浏览
  • 本文介绍瑞芯微开发板/主板Android配置APK默认开启性能模式方法,开启性能模式后,APK的CPU使用优先级会有所提高。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。源码修改修改源码根目录下文件device/rockchip/rk3562/package_performance.xml并添加以下内容,注意"+"号为添加内容,"com.tencent.mm"为AP
    Industio_触觉智能 2025-01-17 14:09 159浏览
  • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
    丙丁先生 2025-01-21 12:10 105浏览
  •  光伏及击穿,都可视之为 复合的逆过程,但是,复合、光伏与击穿,不单是进程的方向相反,偏置状态也不一样,复合的工况,是正偏,光伏是零偏,击穿与漂移则是反偏,光伏的能源是外来的,而击穿消耗的是结区自身和电源的能量,漂移的载流子是 客席载流子,须借外延层才能引入,客席载流子 不受反偏PN结的空乏区阻碍,能漂不能漂,只取决于反偏PN结是否处于外延层的「射程」范围,而穿通的成因,则是因耗尽层的过度扩张,致使跟 端子、外延层或其他空乏区 碰触,当耗尽层融通,耐压 (反向阻断能力) 即告彻底丧失,
    MrCU204 2025-01-17 11:30 176浏览
  • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
    wuliangu 2025-01-21 00:15 153浏览
  • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
    华普微HOPERF 2025-01-20 16:50 70浏览
  • 现在为止,我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接,接下来,让我们趁热打铁,完成剩余配件的连接!注:配件连接前请断开主板所有供电,避免敏感电路损坏!1.1 耳机接口主板有一路OTMP 标准四节耳机座J6,具备进行音频输出及录音功能,接入耳机后声音将优先从耳机输出,如下图所示:1.21.2 相机接口MIPI CSI 接口如上图所示,支持OV5648 和OV8858 摄像头模组。接入摄像头模组后,使用系统相机软件打开相机拍照和录像,如下图所示:1.3 以太网接口主板有一路
    Industio_触觉智能 2025-01-20 11:04 141浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦