<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          詳細(xì)解答!從C++轉(zhuǎn)向Rust需要注意哪些問題?

          共 7645字,需瀏覽 16分鐘

           ·

          2021-10-16 16:15


          導(dǎo)語?|?在日常開發(fā)過程中,若長期使用C++語言,在初次使用Rust的過程中可能會碰到一些問題。本文嘗試從C++的角度來說明在使用Rust時需要特別注意的一些地方,特別是其中的思維方式的轉(zhuǎn)變(mind shift)。


          一、賦值的move語義


          (一)C++ vs Rust


          C++的賦值操作是copy語義,在不考慮優(yōu)化的情況下,從語義的角度理解,賦值后內(nèi)存中的某個對象即變成了兩份。修改新的對象并不會對舊對象產(chǎn)生副作用。

          ?


          而Rust對賦值操作有更加精細(xì)的控制,以下兩條:


          • 對于所有實(shí)現(xiàn)了Copy trait的類型來說,賦值采用了copy語義。


          • 對于其它情況,采用move語義。


          在Rust中直接使用編譯器來保證了move語義,確保變量的值被移出后,不能被再使用,如下例:


          fn main() {    let mut x = 5;    let rx0 = &mut x;    let rx1 = rx0;    println!("test {}", rx0);}


          會產(chǎn)生編譯錯誤:


          error[E0382]: borrow of moved value: `rx0` --> src/main.rs:5:25  |3 |     let rx0 = &mut x;  |         --- move occurs because `rx0` has type `&mut i32`, which does not implement the `Copy` trait4 |     let rx1 = rx0;  |               --- value moved here5 |     println!("test {}", rx0);  |                         ^^^ value borrowed here after move


          明確地說明了原因:變量在移動后又被使用了,在哪兒被使用,以及為什么采用了move語義。


          而在C++中,可以通過禁用class的拷貝構(gòu)造函數(shù)來達(dá)到禁止變量復(fù)制的目的。如以下代碼是編譯不通過的:


          #include 
          using namespace std;
          int main(int argc, const char* argv[]) { auto int_p0 = unique_ptr<int>(new int); auto int_p1 = int_p0; *int_p0 = 5; return 0;}


          在clang++中會產(chǎn)生如下錯誤:


          main.cc:8:10: error: call to implicitly-deleted copy constructor of 'std::__1::unique_ptr >'    auto int_p1 = int_p0;         ^        ~~~~~~/opt/llvm/clang-10.0.1/bin/../include/c++/v1/memory:2513:3: note: copy constructor is implicitly deleted because 'unique_ptr >' has a user-declared move constructor  unique_ptr(unique_ptr&& __u) _NOEXCEPT  ^


          但是只需要將錯誤行改成如下代碼即可以編譯通過:


          auto int_p1 = std::move(int_p0);


          但后果是,程序在對*int_p0進(jìn)行賦值時會產(chǎn)生coredump。這也是Rust所謂的內(nèi)存安全性,即只要沒有使用unsafe,編譯器可以發(fā)現(xiàn)內(nèi)存的錯誤訪問,并拒絕通過編譯。



          (二)引用&T與可變引用&mut T


          還是上面的例子,如果將其中的可變引用改成非可變引用(默認(rèn)形式的引用),如下代碼:


          fn main() {    let x = 5;    let rx0 = &x;    let rx1 = rx0;    println!("test {}", rx0);}


          可以通過編譯。Rust的文檔中有如下說明:


          The following traits are implemented for all &T, regardless of the type of its referent:
          CopyClone (Note that this will not defer to T’s Clone implementation if it exists!)DerefBorrowPointer
          *&mut T references get all of the above except Copy and Clone(to prevent creating multiple simultaneous mutable borrows),plus the following, regardless of the type of its referent:
          DerefMutBorrowMut


          &mut?T相較于&T少實(shí)現(xiàn)了Copy和Clone。因此,對于可變引用&mut?T來說,賦值采用的是move語義,而對于普通引用&T來說采用的是copy語義,所以改成普通引用上面的程序就可以編譯通過了。


          這也是為什么可變引用也被稱之為獨(dú)占引用,因?yàn)槊看螌勺円玫馁x值,都意味著舊變量的失效,這就確保了全局只會存在一份可變引用。


          Rust在這里體現(xiàn)了語言設(shè)計的優(yōu)雅:賦值操作的語義委托到了類型系統(tǒng),通過定義基本的機(jī)制同時約束了自定義類型與內(nèi)建類型的行為,在編譯期完成檢查,而不是需要開發(fā)去記憶各種特例。這在不了解語言的時候會產(chǎn)生學(xué)習(xí)曲線,但是一旦了解了其套路后(Thinking in Rust), 可以顯著地降低編碼過程中的心智負(fù)擔(dān)。



          二、Option與空指針


          (一)enum與match


          在C++中,對于可能存在或不存在的變量,慣常的作法之一是傳入指針 (包括現(xiàn)代C++中智能指針shared_ptrunique_ptr),在處理時,通過檢查指針是否為空來判斷變量是否存在。這是一種非常便利的做法,但是同樣的,此方案在編譯期無法做更多的檢查,最終檢查的責(zé)任交給了開發(fā)。


          Rust對此問題主要使用了兩個機(jī)制:枚舉(enum)模式匹配(match)。相比較C++的enum, Rust的enum更像是C++的union。是 ADT(algebraic data type)中sum types(tagged union)在Rust中的實(shí)現(xiàn)。在Rust中enum可能包括一組類型中的一個,如:


          enum Message {  Quit,  Move {x: i32, y: i32},  Write (String),}


          上面代碼表示,一條消息(Message)可能有三種類型: Quit、Move和Write。當(dāng)類型為Move或者Write時,還可以帶上自己的特定的數(shù)據(jù)。當(dāng)處理Message時,則會使用模式匹配機(jī)制取得具體類型進(jìn)行處理:


          match message {  Message::Quit => todo!(),  Message::Move { x, y } => todo!(),  Message::Write(info) => todo!(),}


          為了避免在修改了enum的定義后,忘記在match中添加相應(yīng)的處理,match會在編譯期要求分支必須覆蓋全部可能的情況。如在Message中新加入一項(xiàng):


          enum Message {  Quit,  Move {x: i32, y: i32},  Write (String),  Send (String), // 新加入}


          再編譯時會出現(xiàn)以下錯誤,提示開發(fā)將Send的處理加入match。


           --> src/main.rs:9:11  |1 | / enum Message {2 | |     Quit,3 | |     Move { x: i32, y: i32 },4 | |     Write(String),5 | |     Send(String),  | |     ---- not covered6 | | }  | |_- `Message` defined here...9 |       match message {  |             ^^^^^^^ pattern `Send(_)` not covered  |  = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms  = note: the matched value is of type `Message`


          由此可見,在C++中,與其最相似的類型其實(shí)是C++17的std::variant,而match機(jī)制類似于std::visit。但Rust在這里做得更完善一些,體現(xiàn)在:


          • 相同的子類型可以因?yàn)門ag的不同出現(xiàn)多次,如上面的Write和Send,子類型都是String。這是std::variant無法直接做到的,除非再封裝一個結(jié)構(gòu)。


          • match會要求分支覆蓋enum所有變體,std::visit也會在編譯期檢查完整的類型覆蓋,但其中類型會考慮C++的隱式類型轉(zhuǎn)換,使用時需要小心。



          (二)Option


          有了上面的預(yù)備知識,現(xiàn)在就可以來了解在Rust中是如何處理空懸指針的問題。先看一下Option的定義:



          pub enum Option<T> {    /// No value    None,    /// Some value `T`    Some(T),}


          在Rust中,對于可選的情景,會定義為該變量類型的Option。假設(shè)某函數(shù)提供從磁盤讀取某個token,該token可能存在或者不存在,那么該函數(shù)的定義會是:


          struct Token { /*...*/ };
          fn load_token() -> Option;


          在使用的時候會采用如下代碼:


          let token = load_token(); // 此時 token 的類型是 Option
          match token { Some(token) => { // 注意這里的 token 是由 Some(token) 這個 pattern 匹配出來的 // 已經(jīng)覆蓋了最外層的 token. 此時 token 的類型是 Token, 已經(jīng) // 確保存在。 todo!() }, None => todo!()}


          可以看到,對于返回Option的情形,無法直接將Option當(dāng)作T來處理,只能使用模式匹配機(jī)制(match,if let,while let等),將T提取出來處理。這一步強(qiáng)制的機(jī)制,確保了對可為空的變量進(jìn)行檢查,避免了對空懸指針的意外訪問。


          相較于使用指針來表達(dá)可選情形,Option的表達(dá)力會更豐富一些,因?yàn)闆]有強(qiáng)制將T轉(zhuǎn)成T*,保留了移動優(yōu)化的可能性;同時,使用專門的類型來表達(dá)可選,在語義上也理加精確一些。


          了解Haskell的同學(xué)可以發(fā)現(xiàn),OptionMaybe如出一轍。事實(shí)上,Rust的類型系統(tǒng),很大程度地受到了Haskell的影響,所以很多地方可以看到Haskell的影子也并不奇怪。學(xué)習(xí)Haskell對理解Rust也會很有幫助。


          最后說明一下,在C++17中加入的std::optional實(shí)現(xiàn)了類似的功能。從接口上說還是像智能指針,使用前需要判斷,否則對std::nullopt進(jìn)行dereference還是會產(chǎn)生運(yùn)行時故障。



          三、迭代器Iterator


          (一)Iterator在Rust中的地位


          Iterator是Rust相對獨(dú)特的功能。對于Rust來說,采用如下的方式去遍歷數(shù)組是低效的:


          let data = vec![1,2,3,4,5];
          for i in 0..data.len() { println!("{}", data[i]);}


          因?yàn)橄虬踩缘?/span>妥協(xié),每次data[i]的操作都會進(jìn)行邊界檢查,顯然這種檢查是不必要的,在性能敏感的場景中也是不可接受的。因此,在Rust中推薦的做法是:


          for v in data {  println!("{}", v);}


          使用迭代器的形式避免了最終取值時的再一次邊界檢查,同時也更加簡潔。由此可見,以地道的Rust風(fēng)格來說,遍歷數(shù)組應(yīng)該使用迭代器來完成,而不是通過遍歷下標(biāo)來進(jìn)行索引。


          對于現(xiàn)代C++ (C++11)來說,也提供了類似的語法方式進(jìn)行容器遍歷:


          for (auto&& v: data) {  // do something for v}



          (二)取得迭代器的三種形式


          對于可以迭代的對象,以std::vec::Vec為例,通常會提供三種方式取得迭代器,如下:


          • iter():取得元素的引用,即&T,非消耗性。


          • iter_mut():取得元素的可變引用,即&mut?T,非消耗性。


          • into_iter():取得元素的所有權(quán),即T,消耗性。


          這里消耗性指的是在迭代完成之后,原來的容器是否還可以繼續(xù)使用。對于into_iter()來說,在迭代過程中已經(jīng)將容器中的所有元素所有權(quán)全部取得,所以最終容器不再持有任何對象,也同時被drop。因此稱之為消耗性的。



          (三)IntoIterator


          對于一般的迭代形式:


          for x in data {}


          Rust期望data是一個實(shí)現(xiàn)了Iterator的對象。否則,會嘗試使用IntoIterator將data轉(zhuǎn)換成`Iterator`對象。所以對于data: Vec來說,實(shí)際展開成了如下代碼:


          for x in IntoIterator::into_iter(data) { }


          這里for ... in語句使用IntoIterator::into_iter獲取了目標(biāo)對象的迭代器。因此,凡是實(shí)現(xiàn)了IntoIterator的類型均可以使用for ... in語句進(jìn)行迭代。


          std::vec::Vec為例,分別為Vec& Vec和&mut Vec實(shí)現(xiàn)了IntoIterator,并分別代理到into_iter()、iter()?和 iter_mut(),以應(yīng)對上面所說的三種不同迭代形式。如下例(清晰起見,將類型注解加上了):


          let mut data: Vec = Vec::from([1,2,3,4]);// 取得引用for v: &i32 in &data {}// 取得可變引用for v: &mut i32 in &mut data {}// 取得所有權(quán)for v: i32 in data {}



          (四)鏈?zhǔn)秸{(diào)用


          在Rust的設(shè)計中,利用Adapter可以靈活而高效地通過Iterator來處理集合。


          Adapter在Rust中指的是一類函數(shù),它們接收一個Iterator并且返回一個Iterator。這樣的接口規(guī)范使用可以通過鏈?zhǔn)秸{(diào)用的方式組合多個Adapter完成復(fù)雜的功能。常見的Adapter包括:map、filter以及filter_map等等。


          除了Adapter,Rust也提供其它一些函數(shù)用于迭代器的最終處理。比如:


          • count


          用于計算元素的個數(shù)。


          • collect


          用于收集迭代器中的元素到某個實(shí)現(xiàn)了FromIterator的類型中去,比如Vec、VecDeque和String等等。


          • reduce


          使用某個函數(shù)對集合進(jìn)行規(guī)約。類似地,也可以使用fold進(jìn)行有初值的規(guī)約。


          可以看到,針對迭代器,Rust提供了豐富的函數(shù)對其處理,具體可以參考文檔。此種編碼風(fēng)格,與舊風(fēng)格的C++很不一樣,轉(zhuǎn)到Rust后在需要對集合進(jìn)行循環(huán)處理的場合,可以有意識地想想,能不能將邏輯寫成迭代器的形式,通??梢缘玫礁雍啙嵉拇a,同時,如前面所說,也可能獲得性能更高的代碼。


          最后提一下,C++社區(qū)也在積極的采納此種代碼風(fēng)格,在C++20中,已經(jīng)將ranges加入標(biāo)準(zhǔn)。其中提供的Range adaptors與Rust的Adapter的概念基本是一樣的。如C++的樣例代碼:


            auto const ints = {0,1,2,3,4,5};  auto even = [](int i) { return 0 == i % 2; };  auto square = [](int i) { return i * i; };
          // "pipe" syntax of composing the views: for (int i : ints | std::views::filter(even) | std::views::transform(square)) { std::cout << i << ' '; }


          寫成Rust則是:


            let ints = vec![0, 1, 2, 3, 4, 5];  let even = |i: &i32| 0 == *i % 2;  let square = |i: i32| i * i;
          for i in ints.into_iter().filter(even).map(square) { println!("{}", i); }



          四、惰性求值—Laziness


          最后需要提一下的是,對于使用鏈?zhǔn)秸{(diào)用的方式將各種Adapter組合的Iterator,其求值是惰性的。即,當(dāng)寫下如下代碼時:


          let v = vec![0,1,2,3,4,5];v.iter().map(|i| println!("{}", i));


          其實(shí)并不會去調(diào)用println將數(shù)據(jù)輸出。Rust文檔的原文是:


          This means that just creating an iterator doesn’t do a whole lot.Nothing really happens until you call next


          即,只有調(diào)用迭代器的next方法,才會依次觸發(fā)各級Iterator的求值。這樣做的好處是:



          (一)性能


          考慮如下代碼:


          let v = vec![0,1,2,3,4,5,6,7,8,9];let even = |i: &i32| 0 == *i % 2;let square = |i: i32| i * i;v.into_iter().filter(even).map(square).take(2);


          如果是eager evaluation,前兩個Adapter,filter(even)和map(square)會分別先執(zhí)行10次和5次,最后才是take(2)取到最開始的兩個元素。如果這個數(shù)組的長度不是10,而100萬,那么這里浪費(fèi)的空間和時間將會是巨大的。同時也會影響響應(yīng)時間,因?yàn)橹挥星懊鎯刹蕉继幚硗戤呏?,才會進(jìn)行到最后一步。


          而采用lazy evaluation時,執(zhí)行會由take(2).next()傳導(dǎo)到map(square)再到filter(even), 最終不論數(shù)組的長度是多少,都只會調(diào)用filter(even)3次,map(square)2次。沒有產(chǎn)生額外的開銷。



          (二)無限迭代


          惰性求值的另一個好處是,使得無限迭代器成為了可能??紤]如下代碼:


          let number = 1..; // 這是一個無限迭代器for n in number.filter(even).take(5) {  println!("{}", n)}


          不會因?yàn)?span style="font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 15px;letter-spacing: 2px;background-color: rgb(255, 255, 255);">filter(even)的調(diào)用而陷入死循環(huán)。而是按需取用。使用此種方法,可以使用遞推公式實(shí)現(xiàn)數(shù)列的迭代器, 并支持各種Adapter的組合:


          pub struct Fib {  n0: u64,  n1: u64,}
          impl Default for Fib { fn default() -> Self { Self { n0: 0, n1: 1 } }}
          impl Iterator for Fib { type Item = u64;
          fn next(&mut self) -> Option<Self::Item> { let n = self.n0 + self.n1; self.n0 = self.n1; self.n1 = n; Some(self.n0) }}
          fn main() { let fib = Fib::default(); let square = |i: u64| i * i; for n in fib.map(square).take(10) { println!("{}", n); }}



          五、總結(jié)


          本文主要是記錄自己從C++轉(zhuǎn)向Rust碰到的一些問題,特別是記錄兩種語言在處理程序設(shè)計中基礎(chǔ)問題的不同套路。這一篇主要介紹了三個主題:move語義、Option和Iterator。由于筆者寫的Rust也不多,所以其中必然會有很多錯誤與不足,發(fā)出來與大家交流,希望大家包涵并不吝指教。


          之后也會以同樣的形式介紹其它主題,比如當(dāng)前心里還想著要記錄的有:錯誤處理、生命周期&借用、interior mutability等。接下來自己爭取將后面的系列完成。



          ?作者簡介


          孟杰

          騰訊后臺開發(fā)工程師

          騰訊后臺開發(fā)工程師,畢業(yè)于中南大學(xué)。目前負(fù)責(zé)騰訊安全流量分析平臺的后臺開發(fā)工作。開發(fā)經(jīng)驗(yàn)豐富,對程序語言、類型系統(tǒng)、編譯等方向很感興趣。



          ?推薦閱讀


          如何保證MySQL和Redis的數(shù)據(jù)一致性?10張圖帶你搞定!

          前端推薦!10分鐘帶你了解Konva運(yùn)行原理

          Golang原生json可以一庫走天下嗎?

          這次全了,8種超詳細(xì)Web跨域解決方案!






          瀏覽 115
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产色情-搜索 | AA片在线播放 | www.一区二区三区在线 | 欧洲 国产精品久久久久久爽爽爽麻豆色哟哟 | 亚洲精品www久久久久久 | 大鸡巴在线视频网站 |