2016年5月25日 星期三

c++ 11 的 move semantic

之前稍微接觸過 c++ 11 move semantic, 那時候似懂非懂不是很清楚這是幹嘛的, 我一開始還把 move semantic 和 return value optimization 搞混了, return value optimization 並不需要 move semantic 才能做到。

再看過 rust 的 move semantic 之後, 才知道這東西的應用, rust 逼開發人員要搞懂這個, 不是壞事, 但我喜歡 c++, 讓我們從 c++ 來理解這個概念。

為了支援 move semantic, c++ 11 引入了 rvalue, 寫成 &&, ex: int &&ri, ri 就是一個 rvalue type。所以 ctor 又多了一個 rvalue 的版本 (ref L28); 當然 operator= 也是, 創造一個 class 要愈寫愈多程式碼。

rust 的這個概念是為了記憶體安全性, c++ 則是為了性能。簡單來說就是希望一個 object 不需要作 copy 的動作。什麼叫作「不需要作 copy 的動作」?

你可能需要自己寫一個類似 std::string (這是一個練習寫 class 的好方法) 的 class 才能理解 move semantic, 我剛好有一個現成的, 拿來理解這樣的行為正好, 順便支援 move semantic。DS::string 就是我的模仿品, 裡頭有個 char *str_, 複製一個 DS::string 需要連 str_ 一起複製, 若是用 move 則是把原本的 s1::str_ 轉到 s2::str_, 那你一定會問:「轉過去了, s1::str_ 剩下什麼? 什麼又是《轉過去》?」答案是你給他什麼就是什麼了。一般會給他 nullptr, 免得解構函式發動時出問題, 因為解構函式會執行 delete [] str_, 我是給 0。這就是為什麼 move 後原來的 s1::str_ 不能用, 不過 c++ compiler 可不像 rust 會幫你擋下來, 我在這個範例上還是照用, 結果就是預期的 null pointer。

複製為什麼比較慢, 看以下的程式碼行為應該就可以理解了:

複製: strcpy(s2::str_, s1:str_)
move : s2::str_ = s1:str_

比較奇怪的是 L118 若改成 void f3(DS::string &&s) 反而不會發動 move ctor, 這裡我不是很理解。

ANS
這個在我反組譯之後有了新的理解, 類似傳 reference (pointer), 根本不會發動 (也用不著) 任何 ctor。

L141 std::move 就是來把這個 object 變成 rvalue, 用 f3((DS::string &&)(s1)); 也可以, 這樣才能發動 move ctor, L28 那個有兩個 && 就是 move ctor; 否則只會發動 copy ctor, 這就沒有節省執行時間了。

mystring.cpp
  1 #include "mystring.h"
  2 #include "myiostream.h"
  3 #include "mem.h"
  4 
  5 #ifdef TEST
  6 #include <cstdio>
  7 using namespace std;
  8 #else
  9 // #define std DS
 10 #endif
 11 
 12 DS::string::string():len_(0), str_(0)
 13 {
 14 #ifdef TEST
 15   std::printf("1 ctor\n");
 16 #endif
 17 }
 18 
 19 DS::string::string(const char *str)
 20 {
 21   generate_string(str, s_strlen(str));
 22 
 23 #ifdef TEST
 24   std::printf("const char *str ctor\n");
 25 #endif
 26 }
 27 
 28 DS::string::string(string &&s)
 29 {
 30   str_ = s.str_;
 31   len_ = s.len_;
 32   s.str_ = 0;
 33   s.len_ = 0;
 34 #ifdef TEST
 35   std::printf("move ctor\n");
 36 #endif
 37 }
 38 
 39 DS::string::string(const string &s)
 40 {
 41   generate_string(s.c_str(), s.length());
 42 
 43 #ifdef TEST
 44   std::printf("copy ctor\n");
 45 #endif
 46 }
 47 
 48 DS::string::~string()
 49 {
 50 #ifdef TEST
 51   std::printf("11 dtor:%s\n", str_);
 52 #endif
 53   delete [] str_;
 54   //cout << "string ~ctor" << endl;
 55 }
 56 
113 
114 #ifdef TEST
115 #include <stdio.h>
116 #include <utility>
117 
118 void f3(DS::string s)
119 {
120   printf("f3 s: %s\n", s.c_str());
121 }
122 
123 DS::string f2()
124 {
125   DS::string s1{"f2"};
126   return s1;
127 }
128 
129 DS::string f1()
130 {
131   std::printf("bb\n");
132   DS::string s1{"return str"};
133   std::printf("ee\n");
134   return s1; 
135 }
136 
137 int main(int argc, char *argv[])
138 {
139   DS::string s1=f2();
140   printf("s1: %s\n", s1.c_str());
141   f3(std::move(s1));
142   printf("s1: %s\n", s1.c_str());
143   printf("s1.length(): %d\n", s1.length());
144 
183   return 0;
184 }
185 #endif

list 1. 執行結果
1 const char *str ctor
2 s1: f2
3 move ctor
4 f3 s: f2
5 11 dtor:f2
6 s1: (null)
7 s1.length(): 0
8 11 dtor:(null)

rust 預設行為是 move semantic, 我不知道這是不是好事情, 不過和 c++ move semantic 一樣, 都需要一點點的專業知識才能理解, 這是他們的高門檻。

最後想問, 大費周張搞了個 move semantic 可以用在哪些地方? 畢竟宣告了一個 object, 幾乎都會在繼續使用, 如果傳給一個 function 後就不能用了, 那寫起來會很不習慣, rust 就是這樣, 容器是一個很好的應用, 把 object 放進容器之後, 就可以用容器的 object, 並不在需要用這個 object 來操作, 所以放進容器的 object 就很適合 move semantic。另外有個網友補充了兩點, 感謝。

下面的參考連結寫的不錯, 不過我有自信你能看懂這篇的話, 應該不需要看以下兩個連結, 但是其提供的《c++ copy and swap idiom 用法》讓我非常受用。

ref:

4 則留言:

  1. 原本 L118 void f3(DS::string s) 時 L141 f3(std::move(s1));s1 是 move 給 f3s,但改成 void f3(DS::string &&s) 的話 s1 就沒有 move 的對象啦。

    另外 move 最重要的應用可能是這兩個:
    1. 以前 type a = create_type(); 可以用 return value optimization 來少一次 ctor,但 type a; a = create_type() 就沒辦法。現在有 move 就可以直接 move 回來,可以少一個 ctor。
    2. 假設寫一個 matrix class,重載 operator+ 的話,免不了會這樣寫 matrix operator+(const matrix& a, const matrix& b) { matrix tmp(a); tmp += b; return tmp; } 在過去為了消掉那個 tmp 通常會用 template expression 這種奇技淫巧。現在有了 move 就不必那樣做了,可以直接把 tmp move 給目的地。

    回覆刪除
  2. 感謝補充:

    "原本 L118 void f3(DS::string s) 時 L141 f3(std::move(s1)); 的 s1 是 move 給 f3 的 s,但改成 void f3(DS::string &&s) 的話 s1 就沒有 move 的對象啦。"

    這個在我反組譯之後有了新的理解, 類似傳 reference (pointer), 根本不會發動 (也用不著) 任何 ctor。

    回覆刪除
  3. 是呀,因為兩邊型別相同,所以單純就是 "forward" 這個變數過去而已。

    回覆刪除
  4. 一個很大的並通常也是主要的用途是在於臨時物件。

    回覆刪除

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。