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