C++11 std::move 強制轉換為右值


【1】std::move

在C++11中,標准庫在<utility>中提供了一個有用的函數std::move。

這個函數的名字很具有迷惑性,因為實際上std::move並不能移動任何東西,它唯一的功能:將一個左值強制轉化為右值引用,繼而可以通過右值引用使用該值,以用於移動語義。

從實現上講,std::move基本等同於一個類型轉換:

static_cast<T&&>(lvalue);

【2】應用注意項

(1)被std::move轉化的左值,其生命期並沒有隨着轉化而改變。

請看這個典型誤用std::move的示例:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class Moveable
 5 {
 6 public:
 7     Moveable(): i(new int(3)) {}
 8     ~Moveable() { delete i; }
 9     Moveable(const Moveable & m): i(new int(*m.i)) { }
10     Moveable(Moveable&& m) : i(m.i)
11     { 
12         m.i = nullptr; 
13     }
14 
15     int* i;
16 }; 
17 
18 int main()
19 {
20     Moveable a;
21     Moveable c(move(a));   // 會調用移動構造函數
22     cout << *a.i << endl;  // 運行時錯誤
23 }

顯然,為類型Moveable定義了移動構造函數。

這個函數定義本身沒有什么問題,但調用的時候,使用了Moveable c(move(a));這樣的語句。

這里的a本來是一個左值變量,通過std::move將其轉換為右值。

這樣一來,a.i就被c的移動構造函數設置為指針空值。

由於a的生命期實際要到main函數結束才結束,那么隨后對表達式*a.i進行計算的時候,就會發生嚴重的運行時錯誤。

當然,標准庫提供該函數的目的不是為了讓程序員搬起石頭砸自己的腳。

事實上,要使用該函數,必須是程序員清楚需要轉換的時候。

比如上例中,程序員應該知道被轉化為右值的a不可以再使用。

(2)通常情況下,需要轉換成為右值引用的還確實是一個生命期即將結束的對象。

比如下例:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class HugeMem
 5 { 
 6 public:
 7     HugeMem(int size) : sz(size > 0 ? size : 1)
 8     { 
 9         c = new int[sz];
10     }
11     ~HugeMem()
12     { 
13         delete []c;
14     }
15     HugeMem(HugeMem && hm) : sz(hm.sz), c(hm.c) 
16     {
17         hm.c = nullptr;
18     }
19     
20     int* c;
21     int sz;
22 }; 
23 
24 class Moveable
25 {
26 public:
27     Moveable() : i(new int(3)), h(1024) { }
28     ~Moveable() { delete i; } 
29     Moveable(Moveable && m) : i(m.i), h(move(m.h)) // 強制轉為右值,以調用移動構造函數
30     {
31         m.i = nullptr;
32     } 
33     
34     int* i;
35     HugeMem h;
36 };
37 
38 Moveable GetTemp() 
39 { 
40     Moveable tmp = Moveable();
41     cout << hex << "Huge Mem from " << __func__ << " @" << tmp. h. c << endl; // Huge Mem from GetTemp @0x0086E490
42     return tmp;
43 } 
44 
45 int main()
46 { 
47     Moveable a(GetTemp());
48     cout << hex << "Huge Mem from " << __func__ << " @" << a. h. c << endl; // Huge Mem from main @0x0086E490
49 }

定義了兩個類型:HugeMem和Moveable,其中Moveable包含了一個HugeMem的對象。

在Moveable的移動構造函數中,我們就看到了std::move函數的使用。

該函數將m.h強制轉化為右值,以迫使Moveable中的h能夠實現移動構造。

這里可以使用std::move,是因為m.h是m的成員,既然m將在表達式結束后被析構,其成員也自然會被析構,因此不存在生存期不合理的問題。

關於std::move使用的必要性問題,在這里再贅述(可參見隨筆《移動語義》)一遍:

如果不使用std::move(m.h)這樣的表達式,而是直接使用m.h這個表達式,由於m.h是個左值,就會導致調用HugeMem的拷貝構造函數來構造Moveable的成員h。

如果是這樣,移動語義就沒有能夠成功地向類的成員傳遞。換言之,還是會由於拷貝而導致一定的性能上的損失。

(3)如何判斷一個類型是否具有可移動構造函數?

在標准庫的頭文件<type_traits>里,我們還可以通過一些輔助的模板類來判斷一個類型是否是可以移動的。比如:

is_move_constructible、

is_trivially_move_constructible、

is_nothrow_move_constructible,使用方法仍然是使用其成員value。示例代碼:

 1 #include <iostream>
 2 #include <type_traits>
 3 using namespace std;
 4 
 5 class HugeMem
 6 { 
 7 public:
 8     HugeMem(int size) : sz(size > 0 ? size : 1)
 9     { 
10         c = new int[sz];
11     }
12     ~HugeMem()
13     { 
14         delete [] c;
15     }
16     HugeMem(HugeMem && hm) : sz(hm.sz), c(hm.c) 
17     {
18         hm.c = nullptr;
19     }
20     
21     int* c;
22     int sz;
23 }; 
24 
25 class Moveable
26 {
27 public:
28     Moveable() : i(new int(3)), h(1024) { }
29     ~Moveable() { delete i; } 
30     Moveable(Moveable && m) noexcept : i(m.i), h(move(m.h))  // 強制轉為右值,以調用移動構造函數
31     {
32         m.i = nullptr;
33     } 
34     
35     int* i;
36     HugeMem h;
37 };
38 
39 int main()
40 { 
41     cout << is_move_constructible<HugeMem>::value << endl;             // 1 測試類型是否具有移動構造函數
42     cout << is_move_constructible<Moveable>::value << endl;            // 1
43     cout << is_trivially_move_constructible<HugeMem>::value << endl;   // 0 測試類型是否具有普通移動構造函數
44     cout << is_trivially_move_constructible<Moveable>::value << endl;  // 0
45     cout << is_nothrow_move_constructible<HugeMem>::value << endl;     // 0 測試類型是否具有nothrow移動構造函數
46     cout << is_nothrow_move_constructible<Moveable>::value << endl;    // 1
47 }

可以判斷是否具有移動構造函數、是否具有普通移動構造函數、是否具有不拋異常的移動構造函數。

(4)移動語義對泛型編程的積極意義

一個比較典型的應用是可以實現高性能的置換(swap)函數。

如下代碼:

template <class T>
void swap(T& a, T& b)
{
    T tmp(move(a)); 
    a = move(b); 
    b = move(tmp); 
}

如果T是可以移動的,那么移動構造和移動賦值將會被用於這個置換。

代碼中,a先將自己的資源交給tmp,隨后b再將資源交給a,tmp隨后又將從a中得到的資源交給b,從而完成了一個置換動作。

整個過程,代碼都只會按照移動語義進行指針交換,不會有資源的釋放與申請。

而如果T不可移動卻是可拷貝的,那么拷貝語義會被用來進行置換。這就跟普通的置換語句是相同的了。

因此在移動語義的支持下,我們僅僅通過一個通用的模板,就可能更高效地完成置換。

綜上所述:

實際上,為了保證移動語義的傳遞,程序員在編寫移動構造函數的時候,應該總是記得使用std::move轉換擁有形如堆內存、文件句柄等資源的成員為右值。

這樣一來,如果成員支持移動構造的話,就可以實現其移動語義。

而即使成員沒有移動構造函數,那么接受常量左值的構造函數版本也會輕松地實現拷貝構造,因此也不會引起大的問題。

 

good good study, day day up.

順序 選擇 循環 總結


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM