為什么要用移動語義
先看看下面的代碼
// 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時不分配內存,即不使用所謂的拷貝構造函數呢?
那有人又要問了,為什么不用指針會給GetTemp的參數?首先需要指針或引用的方法而不返回值的話,通常需要很多條語句來完成上面的工作。
例如以下語句:
Caculate(GetTeam(),SomeOther(mabe(),UseFul(Value,2)));
但是如果通過傳引用和指針而不返回值的話,那需要多條語句來完成上面的工作:
string * a,vector b;//事先聲明一些變量用於傳遞返回值
...
UseFul(Value,2,a)
SomeOther(maybe(),a,b);
Caculate(GetTemp(),b)
此時移動語義應運而生。
要想了解移動語義,則需要從左值和右值說起。
左值和右值
判斷左值和右值的方法有兩種
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