|
本文經(jīng)授權(quán)轉(zhuǎn)自公眾號(hào)CSDN(ID:CSDNnews)
整理 | 鄭麗媛
【編者按】自從美國(guó)白宮對(duì)開(kāi)發(fā)者呼吁,“停止使用 C 和 C++,改用 Rust 等內(nèi)存安全編程語(yǔ)言”后,兩方之間從未停止的爭(zhēng)論就被推到了一個(gè)新高度。而在這之中,也有部分 C++ 開(kāi)發(fā)者提議:或許 Rust 中的一些概念,可以試著運(yùn)用到 C++ 編程中?
近日,一位開(kāi)發(fā)者(ID:delta242)在 Reddit 上發(fā)了一篇長(zhǎng)文《在 C++ 中應(yīng)用 Rust 的概念》,里面提到了一些可用于改善 C++ 代碼的 Rust 概念,引來(lái)了諸多關(guān)注和討論。
根據(jù)他在開(kāi)篇的介紹,“雖然我不是 Rust 專家,但我很喜歡這門語(yǔ)言的許多概念。在日常編程中,我主要用 C++,而現(xiàn)在我經(jīng)常會(huì)運(yùn)用一些 Rust 的概念來(lái)改善我的 C++ 代碼”,可以看出,下面是他親身實(shí)踐過(guò)的、可用于優(yōu)化 C++ 代碼的 Rust 概念。
1、在 C++ 中,如何應(yīng)用 Rust 的概念?
(以下為他的長(zhǎng)文翻譯:)
(1)帶值的枚舉
我很喜歡 Rust 的枚舉,因?yàn)槟憧梢越o枚舉常量賦值(例如,Option 枚舉中有一個(gè)沒(méi)有值的 None 和一個(gè)有值的 Some)。在類型理論中,這通常被稱為代數(shù)數(shù)據(jù)類型,而在 C++ 中,我們有 variants,可以定義輔助結(jié)構(gòu)體來(lái)實(shí)現(xiàn)類似的功能:
struct Some { T value; };struct None { };using Optional = std::variant;(注:這個(gè)例子可能有點(diǎn)蠢,因?yàn)?std::optional 要好得多。但對(duì)于更復(fù)雜的類型來(lái)說(shuō),這具有一定參考意義。)
(2)CRTP 和 Traits
在 Rust 中,Traits 用于定義類型的共享功能。而在 C++ 中,我們可以用 CRTP 在編譯時(shí)強(qiáng)制類實(shí)現(xiàn)特定的函數(shù)來(lái)實(shí)現(xiàn)靜態(tài)多態(tài)性。CRTP 還允許在基類中實(shí)現(xiàn)默認(rèn)功能,我以前曾用這種方法來(lái)定義迭代器類型,只要基類實(shí)現(xiàn)了 operator[],就可以減少大量模板代碼的編寫。
(3)字符串格式化
在 C++ 中,如果向 std::format 傳遞的參數(shù)數(shù)量多于格式字符串中的占位符,并不會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤。我曾經(jīng)遇到過(guò)這樣的 bug,例如由于缺少占位符,日志消息中缺少了某些信息,導(dǎo)致與代碼中不一致。
而這個(gè)情況如果放在 Rust 中,就會(huì)產(chǎn)生編譯時(shí)錯(cuò)誤。所以這對(duì)于 C++ 來(lái)說(shuō),將是一個(gè)簡(jiǎn)單而實(shí)用的改進(jìn),有助于提高代碼質(zhì)量和開(kāi)發(fā)效率。
(4)擁有 Mutex
在 Rust 中,Mutex 類型擁有受保護(hù)的值。我非常喜歡這個(gè)概念,這樣不獲取 Mutex 就無(wú)法訪問(wèn)受保護(hù)的值(這在 C++ 中經(jīng)常發(fā)生)。有一個(gè)簡(jiǎn)單的技巧來(lái)實(shí)現(xiàn)類似效果,那就是在 C++ 中寫一個(gè)具有 lock 函數(shù)的封裝 Mutex 類,該函數(shù)將接受一個(gè)帶有對(duì)受保護(hù)值的引用的 lambda 表達(dá)式作為參數(shù)。由于 Rust 中有借用檢查器,這樣的操作總是安全的,而在 C++ 中,誤用很容易再次導(dǎo)致競(jìng)爭(zhēng)條件,但至少通過(guò)這樣的封裝器,這種情況就不那么容易發(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á)式來(lái)使用立即調(diào)用的函數(shù)表達(dá)式(IIFE)來(lái)達(dá)到同樣的效果:
auto value = [] { // Complex initializer return result;}(); // notice the invocation以上,就是我現(xiàn)在能想到的。
2、“Rust 讓我成為了一名更好的 C++ 開(kāi)發(fā)者”
在這篇長(zhǎng)文下,不少開(kāi)發(fā)者也分享了自己在 C++ 編程中借鑒 Rust 概念的心得,甚至直言“Rust 讓我成為了一名更好的 C++ 開(kāi)發(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)“重載非常好,我覺(jué)得它可以成為 STL 的一部分。此外,有了 C++20 模板化的 lambdas,還可以編寫一些非;ㄉ诘拇a。”
visit( overloaded { [] T>(T value) {} [](auto other) {} }, value)對(duì)此,一位開(kāi)發(fā)者感慨:“這正是我希望看到的,雖然我不喜歡 Rust,但它確實(shí)有一些 C++ 可以借鑒的做法,更安全總歸是好的!
3、在 C++ 中應(yīng)用 Rust 概念的一些失敗案例
不過(guò)與此同時(shí),也有開(kāi)發(fā)者提醒“必須小心”:以 Rust 的 Mutex 為例,當(dāng)你訪問(wèn) Mutex 中的數(shù)據(jù)時(shí),不可能將該指針存儲(chǔ)下來(lái),然后在解鎖 Mutex 后再訪問(wèn)數(shù)據(jù)(忽略特殊情況)。你可以在 C++ 中實(shí)現(xiàn)一個(gè)擁有 Mutex 的類,但編譯器不會(huì)在意你是否在鎖的作用域之外持有一個(gè)指向受保護(hù)數(shù)據(jù)的指針,并在未受保護(hù)的情況下訪問(wèn)它。
針對(duì)這個(gè)話題,開(kāi)源搜索引擎 Meilisearch 的高級(jí)工程師 Louis Dureuil 曾寫過(guò)一篇相關(guān)文章《這對(duì) C++ 來(lái)說(shuō)太危險(xiǎn)了》:“一些設(shè)計(jì)模式之所以實(shí)用,歸功于 Rust 的內(nèi)存安全性,而在 C++ 中使用則過(guò)于危險(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!()}后來(lái)他發(fā)現(xiàn),對(duì)某些輸入進(jìn)行解析需要很長(zhǎng)時(shí)間,于是決定通過(guò)通道將輸入發(fā)送到另一個(gè)線程,并通過(guò)另一個(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), } } }也就是說(shuō),只要不需要引用原子操作的計(jì)數(shù),就可以使用 Rc。但當(dāng)需要線程安全時(shí),編譯器會(huì)強(qiáng)制 Louis Dureuil 切換到 Arc,并帶來(lái)了原子引用計(jì)數(shù)的開(kāi)銷。
Louis Dureuil 指出,這個(gè)原則也深受 C++ 開(kāi)發(fā)者的喜愛(ài)。但與 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)和薪資要求來(lái)找合適的崗位。
經(jīng)常遇到一些想回老家或者二三線城市的同學(xué)苦于沒(méi)有合適的去處,打開(kāi)boss直聘和獵聘網(wǎng)這些招聘類軟件,結(jié)果發(fā)現(xiàn)好的崗位基本都集中在一線城市,很少看到那種有二三線城市的招聘崗位。 |
|