[心得] c++ 11 的 move semantic

作者: descent (「雄辯是銀,沉默是金」)   2016-06-05 23:44:54
個人學習速度慢, 2016 了才學到 2011 的東西。
精華區有個很仔細的版本, 我這篇不那麼長。
之前稍微接觸過 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 reference,
寫成 &&, ex: int &&ri, ri 就是一個 rvalue reference 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 用法 ( https://goo.gl/NU7HmE )》讓我非
常受用。
ref:
翻:怎理解 C++ 11中的move(基)
作者: chiwa (我是青蛙,不是王子^_^)   2016-06-06 20:10:00
rvalue 不等於 rvalue reference
作者: Dannvix (Dan)   2016-06-06 20:17:00
延伸閱讀Scott Meyers on Uni. Ref. http://j.mp/1su76iV

Links booklink

Contact Us: admin [ a t ] ucptt.com