電子產(chǎn)業(yè)一站式賦能平臺(tái)

PCB聯(lián)盟網(wǎng)

搜索
查看: 50|回復(fù): 0
收起左側(cè)

“Rust 思維下的 C++ 編程”:在 C++ 中,如何應(yīng)用 Rust 中的概念?

[復(fù)制鏈接]

435

主題

435

帖子

3093

積分

四級(jí)會(huì)員

Rank: 4

積分
3093
跳轉(zhuǎn)到指定樓層
樓主
發(fā)表于 2024-9-2 09:00:00 | 只看該作者 回帖獎(jiǎng)勵(lì) |倒序?yàn)g覽 |閱讀模式
本文經(jīng)授權(quán)轉(zhuǎn)自公眾號(hào)CSDN(ID:CSDNnews)
整理 | 鄭麗媛

【編者按】自從美國(guó)白宮對(duì)開發(fā)者呼吁,“停止使用 C 和 C++,改用 Rust 等內(nèi)存安全編程語(yǔ)言”后,兩方之間從未停止的爭(zhēng)論就被推到了一個(gè)新高度。而在這之中,也有部分 C++ 開發(fā)者提議:或許 Rust 中的一些概念,可以試著運(yùn)用到 C++ 編程中?
近日,一位開發(fā)者(ID:delta242)在 Reddit 上發(fā)了一篇長(zhǎng)文《在 C++ 中應(yīng)用 Rust 的概念》,里面提到了一些可用于改善 C++ 代碼的 Rust 概念,引來了諸多關(guān)注和討論。
根據(jù)他在開篇的介紹,“雖然我不是 Rust 專家,但我很喜歡這門語(yǔ)言的許多概念。在日常編程中,我主要用 C++,而現(xiàn)在我經(jīng)常會(huì)運(yùn)用一些 Rust 的概念來改善我的 C++ 代碼”,可以看出,下面是他親身實(shí)踐過的、可用于優(yōu)化 C++ 代碼的 Rust 概念。

1、在 C++ 中,如何應(yīng)用 Rust 的概念?
(以下為他的長(zhǎng)文翻譯:)
(1)帶值的枚舉
我很喜歡 Rust 的枚舉,因?yàn)槟憧梢越o枚舉常量賦值(例如,Option 枚舉中有一個(gè)沒有值的 None 和一個(gè)有值的 Some)。在類型理論中,這通常被稱為代數(shù)數(shù)據(jù)類型,而在 C++ 中,我們有 variants,可以定義輔助結(jié)構(gòu)體來實(shí)現(xiàn)類似的功能:
  • struct Some { T value; };struct None { };using Optional = std::variant;(注:這個(gè)例子可能有點(diǎn)蠢,因?yàn)?std::optional 要好得多。但對(duì)于更復(fù)雜的類型來說,這具有一定參考意義。)
    (2)CRTP 和 Traits
    在 Rust 中,Traits 用于定義類型的共享功能。而在 C++ 中,我們可以用 CRTP 在編譯時(shí)強(qiáng)制類實(shí)現(xiàn)特定的函數(shù)來實(shí)現(xiàn)靜態(tài)多態(tài)性。CRTP 還允許在基類中實(shí)現(xiàn)默認(rèn)功能,我以前曾用這種方法來定義迭代器類型,只要基類實(shí)現(xiàn)了 operator[],就可以減少大量模板代碼的編寫。
    (3)字符串格式化
    在 C++ 中,如果向 std::format 傳遞的參數(shù)數(shù)量多于格式字符串中的占位符,并不會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤。我曾經(jīng)遇到過這樣的 bug,例如由于缺少占位符,日志消息中缺少了某些信息,導(dǎo)致與代碼中不一致。
    而這個(gè)情況如果放在 Rust 中,就會(huì)產(chǎn)生編譯時(shí)錯(cuò)誤。所以這對(duì)于 C++ 來說,將是一個(gè)簡(jiǎn)單而實(shí)用的改進(jìn),有助于提高代碼質(zhì)量和開發(fā)效率。
    (4)擁有 Mutex
    在 Rust 中,Mutex 類型擁有受保護(hù)的值。我非常喜歡這個(gè)概念,這樣不獲取 Mutex 就無法訪問受保護(hù)的值(這在 C++ 中經(jīng)常發(fā)生)。有一個(gè)簡(jiǎn)單的技巧來實(shí)現(xiàn)類似效果,那就是在 C++ 中寫一個(gè)具有 lock 函數(shù)的封裝 Mutex 類,該函數(shù)將接受一個(gè)帶有對(duì)受保護(hù)值的引用的 lambda 表達(dá)式作為參數(shù)。由于 Rust 中有借用檢查器,這樣的操作總是安全的,而在 C++ 中,誤用很容易再次導(dǎo)致競(jìng)爭(zhēng)條件,但至少通過這樣的封裝器,這種情況就不那么容易發(fā)生了。
    (5)內(nèi)部可變性
    Rust 在安全的情況下會(huì)使用內(nèi)部可變性(即使變量是 const),例如當(dāng)一個(gè)值受 Mutex 保護(hù)時(shí)。在 C++ 中,我們也可以采用類似的想法,例如“const 表示線程安全”。
    (6)IIFE
    在 Rust 中,每個(gè)作用域都是一個(gè)表達(dá)式,這樣可以很好地將變量限制在更小的作用域中。而在 C++ 中,我們可以用 lamdas 表達(dá)式來使用立即調(diào)用的函數(shù)表達(dá)式(IIFE)來達(dá)到同樣的效果:
  • auto value = [] {  // Complex initializer  return result;}(); // notice the invocation以上,就是我現(xiàn)在能想到的。

    2、“Rust 讓我成為了一名更好的 C++ 開發(fā)者”
    在這篇長(zhǎng)文下,不少開發(fā)者也分享了自己在 C++ 編程中借鑒 Rust 概念的心得,甚至直言“Rust 讓我成為了一名更好的 C++ 開發(fā)者”。
    (1)“最近,我養(yǎng)成了在 C++ 中使用“match”宏的這個(gè)習(xí)慣,我很喜歡。”
  • 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,還可以編寫一些非;ㄉ诘拇a。”
  • visit(  overloaded {    [] T>(T value) {}    [](auto other) {}  }, value)對(duì)此,一位開發(fā)者感慨:“這正是我希望看到的,雖然我不喜歡 Rust,但它確實(shí)有一些 C++ 可以借鑒的做法,更安全總歸是好的。”

    3、在 C++ 中應(yīng)用 Rust 概念的一些失敗案例
    不過與此同時(shí),也有開發(fā)者提醒“必須小心”:以 Rust 的 Mutex 為例,當(dāng)你訪問 Mutex 中的數(shù)據(jù)時(shí),不可能將該指針存儲(chǔ)下來,然后在解鎖 Mutex 后再訪問數(shù)據(jù)(忽略特殊情況)。你可以在 C++ 中實(shí)現(xiàn)一個(gè)擁有 Mutex 的類,但編譯器不會(huì)在意你是否在鎖的作用域之外持有一個(gè)指向受保護(hù)數(shù)據(jù)的指針,并在未受保護(hù)的情況下訪問它。
    針對(duì)這個(gè)話題,開源搜索引擎 Meilisearch 的高級(jí)工程師 Louis Dureuil 曾寫過一篇相關(guān)文章《這對(duì) C++ 來說太危險(xiǎn)了》:“一些設(shè)計(jì)模式之所以實(shí)用,歸功于 Rust 的內(nèi)存安全性,而在 C++ 中使用則過于危險(xiǎn)。”
    在文中,Louis Dureuil 分享了他在 C++ 中應(yīng)用 Rust 概念的失敗案例。
    當(dāng)時(shí),他正在用 Rust 編寫一個(gè)內(nèi)部庫(kù),其中有一個(gè)他希望能克隆、而不會(huì)復(fù)制其中數(shù)據(jù)的錯(cuò)誤類型。在 Rust 中,這需要使用引用計(jì)數(shù)指針,比如 Rc。他編寫了一個(gè)錯(cuò)誤類型,將其用作可能發(fā)生錯(cuò)誤的函數(shù)的錯(cuò)誤變體,繼續(xù)了他的工作。
  • struct Error {    data: Rc,}
    pub type Response = Result;
    fn parse(input: Input) -> Response {    todo!()}后來他發(fā)現(xiàn),對(duì)某些輸入進(jìn)行解析需要很長(zhǎng)時(shí)間,于是決定通過通道將輸入發(fā)送到另一個(gè)線程,并通過另一個(gè)通道獲取響應(yīng),這樣長(zhǎng)時(shí)間的解析就不會(huì)阻塞主線程。
  • enum Command {    Input(Input),    Exit,}
    pub enum RequestStatus {    Completed(Response),    Running,}
    pub struct Parser {    command_sender: Sender,    response_receiver: Receiver,    cached_result: HashMap[I],}
    impl Parser {    pub fn new() -> Self {        let (command_sender, command_receiver) = channel::();        let (response_sender, response_receiver) = channel::();
            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()    }}然而,在進(jìn)行這一更改時(shí),Louis Dureuil 收到了以下錯(cuò)誤信息:
  • 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)>` 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`正如編譯器所解釋的那樣,這是因?yàn)?Rc 類型不支持在線程之間發(fā)送,因?yàn)檫@樣會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)。實(shí)際上,Rc 中的引用計(jì)數(shù)并不以原子方式進(jìn)行操作,而是使用常規(guī)的整數(shù)操作。
    為了實(shí)現(xiàn)線程安全的引用計(jì)數(shù),Rust 提供了另一種類型 Arc,它使用原子引用計(jì)數(shù),而將代碼修改為使用 Arc 非常簡(jiǎn)單:
  • 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),             }         }     }也就是說,只要不需要引用原子操作的計(jì)數(shù),就可以使用 Rc。但當(dāng)需要線程安全時(shí),編譯器會(huì)強(qiáng)制 Louis Dureuil 切換到 Arc,并帶來了原子引用計(jì)數(shù)的開銷。
    Louis Dureuil 指出,這個(gè)原則也深受 C++ 開發(fā)者的喜愛。但與 Rust 完全不同的是,在 C++ 中,標(biāo)準(zhǔn)庫(kù)中只有帶有原子引用計(jì)數(shù)的 shared_ptr,它相當(dāng)于 Arc,而不是 Rc——所以,即使你不使用原子操作,也仍要為原子引用計(jì)數(shù)付出代價(jià)。
    最后,一句話總結(jié):在 C++ 中適當(dāng)應(yīng)用 Rust 概念固然不錯(cuò),但切記不要根據(jù)在 Rust 中會(huì)發(fā)生的情況,對(duì) C++ 也做出相同的假設(shè)。
    參考鏈接:
    https://www.reddit.com/r/cpp/comments/1bx7wjm/applying_concepts_from_rust_in_c/
    https://blog.dureuill.net/articles/too-dangerous-cpp/
    本文轉(zhuǎn)自公眾號(hào)“CSDN”,ID:CSDNnews分享個(gè)群友推薦的招聘類小程序這里分享一個(gè)交流群群友推薦的招聘小程序,其中有很多二三線城市,比如赤峰、保定、阿克蘇之類的三四線城市,還支持按照崗位、地點(diǎn)和薪資要求來找合適的崗位。
    經(jīng)常遇到一些想回老家或者二三線城市的同學(xué)苦于沒有合適的去處,打開boss直聘和獵聘網(wǎng)這些招聘類軟件,結(jié)果發(fā)現(xiàn)好的崗位基本都集中在一線城市,很少看到那種有二三線城市的招聘崗位。
  • 發(fā)表回復(fù)

    本版積分規(guī)則

    關(guān)閉

    站長(zhǎng)推薦上一條 /1 下一條


    聯(lián)系客服 關(guān)注微信 下載APP 返回頂部 返回列表