標籤:類型轉換 技術分享 控制 無法 釋放 val void 引用 turn
為什麼要用移動語意
先看看下面的代碼
// rvalue_reference.cpp : 定義控制台應用程式的進入點。//#include "stdafx.h"#include <iostream>class HugeMem{public: HugeMem(int size) : sz(size) { pIntData = new int[sz]; } HugeMem(const HugeMem & h) : sz(h.sz) { pIntData = new int[sz]; for (int i = 0; i < sz; i++) pIntData[i] = h.pIntData[i]; } ~HugeMem() { delete pIntData []; } int *pIntData; int sz;};HugeMem GetTemp(){ return HugeMem(1024);}int _tmain(int argc, _TCHAR* argv[]){ HugeMem a = GetTemp(); return 0;}
以上代碼拷貝建構函式會被調用兩次,一次是從GetTemp函數中有HugeMem()產生的一個臨時值用作傳回值,另外一次則由臨時值構造出main中的變數a。解構函式調用了三次。這個過程如果指標指向非常大的記憶體時拷貝構造 的代價相當昂貴。而令人堪憂的是:臨時變數的生產和銷毀以及拷貝構造的發生對於程式員來說基本上是透明的,不會影響程式的正確性,因而即使該問題導致效能不佳,也不易被程式員察覺。
而關鍵的問題是,臨時對象在構造和釋放時,一去一來似乎並沒有太大意義,那麼我們是否可以在臨時物件建構a時不分配記憶體,即不使用所謂的拷貝建構函式呢?
此時移動語意應運而生。
要想瞭解移動語意,則需要從左值和右值說起。
左值和右值
判斷左值和右值的方法有兩種
1.在等號左邊的值就稱為左值而在等號右邊的稱為右值
2.另外在c++中還有一種判別方法就是可以取地址,有名的就是左值,不能取地址,沒有名的就是右值
例如:
a = b + c
a在等號左邊被值為左值
b+c在等號右邊被稱為右值
a有名,可以取地址稱為左值
b+c沒名,不可以取地址稱為右值
純右值、將亡值
在c++ 11中右值被分為純右值和將亡值
其中純右值就是c++98中的標準右值用於標記臨時變數或不根對象有關的值。
將亡值是在c++ 11中跟右值引用相關的運算式,這種運算式通常是被移動的對象(移為他用),比如返回右值引用T&&的函數傳回值 ,std::move的傳回值,或者轉換為T&&的類型轉換函式的傳回值,而剩餘的可以標識函數,對象的值都屬於左值。
在c++ 11中,所有的值必須為左值、純右值、將亡值的三種的任一一種。
右值引用
c++ 11中,右值引用就是對一個右值進行引用 的類型,事實上,通常右值不巨有名稱,我們只能通過引用來找到他的存在 一般情況下,我們只能從右值運算式獲得他的引用
int && c = RetValue();
c++ 98的引用(左值引用)
c++ 98中的引用一般都稱為左值引用
int d = 100;
int & dd = d;//這種是c++98裡面的左值引用
左值引用是具名變數值的別名,右值引用是則是不具名(匿名)變數的別名右值引用相當於給右值“續期”
在上面右值引用的例子中,RetValue()在函數返回右值運算式結束後,他的生命也就終結了,而右值引用的聲明,又給他“重獲新生”,他的生命週期將與他的右值引用c的生命週期一樣,只要c還活前些,該右值臨時變數都會一直存活下去
所以相比以下聲明
CObj c = RetValue()
CObj && c = RetValue()
不使用右值引用就會多一次構造和析構
聲明一個右值引用的類型前提是RetValue()返回的是一個右值,通常情況下右值引用是不能夠綁定左值的。比如下面的代碼是無法通過編譯的
int d = 100;
int & dd = d;//這種是c++98裡面的左值引用
int && dd2 = d;//無法通過編譯 ,編譯器提示無法將右值引用綁定到左值
相應的,是否可以將一個左值引用綁定一個右值呢,例如:
int & d = 100;//不可以,編譯錯誤
以上代碼說明相應的左值引用無法綁定一個右值
但是c98有一種常左值引用就是const T&,這在c98裡是“萬能”參考型別,他可以接受,非常量左值,常量左值,右值對其初始化例如:
int d = 100;
const int e = 100;
const int & c = d;//接受一個非常量左值
const int & v = e;//接受一個常量左值
const int & z = 3 + 4;//接受一個右值
c98中常量左值引用經常被用於降低臨時對象的開銷,例如:
int Add(const T & s1,const T & s2);
在c++ 11中,也可以用右值引用來做函數的參數,這樣同樣可以降低臨時變數開銷例如:
void Test(CTestObj && a) //在函數內部還可以修改引用a 的值
就本例而言我們可以這樣寫這個函數
void Test(CTestObj && a)
{
CTestObj b = std::move(a) ;
}
std::move的作用時,強制使一個左值成為右值,使用移動語意的前提是CTestObj還需要添加一個右值引用為參數的移動建構函式。
這樣一來CTestObj類的臨時對象(即ReturnValue的返回的臨時值)包含一些大塊指標,就可以從臨時對象中“竊”為已用。事實上右值引用的存在從來就是和移動語意有關。
假如我們沒有為CTestObj聲明一個移動建構函式,而只聲明一個常量左值為參數的建構函式會發生什嗎?如同我們前面所說的常量左值引用是一個萬能的引用,無論常量左值,非常量左值,右值都可以。那麼如果我們不聲明移動建構函式,下列語句:
CTestObj b = std::move(a)
將調用常量左值引用為參數的拷貝建構函式。這是一種非常安全的設計----移動不成至少還可以執行拷貝。因此程式頁會為聲明了移動建構函式的類聲明一個常量左值引用為參數的拷貝建構函式,以保證移動不成時可以拷貝構造 。
為了語義完整c++ 11中還存在一個常量右值引用例如:
const T && a = ReturnRValue()
不過常量右值引用一般沒有用武之地。
std::move強制轉化為右值
std::move並不能移動任何東西,他唯一的功能是將左值轉化為右值,繼而我們可以用右值引用引用這個值,以用於移動語意。
// rvalue_reference.cpp : 定義控制台應用程式的進入點。//#include "stdafx.h"#include <iostream>class Moveable{public: Moveable() :i(new int(3)) {} ~Moveable() { delete i;} Moveable(const Moveable & s) : i(new int(*s.i)) {} Moveable(Moveable && s) : i(s.i) { s.i = nullptr; } int * i;};int _tmain(int argc, _TCHAR* argv[]){ Moveable a; Moveable c = a; //這裡會調用拷貝建構函式 Moveable d; Moveable e(std::move(d));//這裡會調用移動建構函式 std::cout << *d.i << std::endl;//這裡會出現違規訪問此時i指標為nullptr return 0;}
以上是典型的誤用std::move的例子,事實上要使用該必須是程式員清楚需要轉換的時候。比如上面代碼中程式員應該知道被轉化為右值的a不可以再使用。我們需要轉化為右值引用還是應該是一個確實生命週期即將結束的對象。
下面是一個正確使用std::move的例子
// rvalue_reference.cpp : 定義控制台應用程式的進入點。//#include "stdafx.h"#include <iostream>class HugeMem{public: HugeMem(int size) : sz(size > 0 ? size : 1) { c = new int[sz]; } ~HugeMem() { delete [] c; } HugeMem(HugeMem && hm) : sz(hm.sz), c(hm.c) { hm.c = nullptr; } int *c; int sz;};class Moveable{public: Moveable() :i(new int(3)),h(1024) {} ~Moveable() { delete i;} Moveable(Moveable && s) : i(s.i), h(std::move(s.h)) { s.i = nullptr; } HugeMem h; int * i;};Moveable GetTemp(){ Moveable temp = Moveable(); std::cout << std::hex << "huge mem GetTemp:" << " @" << temp.h.c << std::endl; return temp;}int _tmain(int argc, _TCHAR* argv[]){ Moveable a(GetTemp()); std::cout << std::hex << "huge mem main:" << " @" << a.h.c << std::endl; return 0;}
運行結果:
由結果可以看出移動語意解決了拷貝語文帶來的拷貝開銷,在拷貝記憶體較大時,效能猶為明顯
如果沒有std::move會怎樣?
因為移動建構函式的參數是 (T && b),右值引用參數可以接收的值為非常量右值,其它值都不可以轉化為右值引用參數,所以必須要用到std::move
c++ 11 移動語意、std::move 左值、右值、將亡值、純右值、右值引用