C++11:移動語義與完美轉發

轉自html

http://www.javashuo.com/article/p-ymiycxlp-e.html安全

C++11 引入的新特性中,除了併發內存模型和相關設施,這些高帥富以外,最引人入勝且接地氣的特性就要屬『右值引用』了(rvalue reference)。加入右值引用的動機在於效率:減小沒必要要的資源拷貝。考慮下面的程序:併發

std::vector<string> v;
v.push_back("string");

首先看push_back:函數

push_back(const T& x)優化

實際上傳進來的是引用ui

而在push_back中構造時,使用的是construct:this

void construct(T1* p,const T2& value)
{
  new(p) T1(value);  
}

  T1*爲迭代器所指的位置,而構造的值value也是引用。T1爲迭代器類型,而p爲該迭代器所處的地址,也就是說construct其實是在迭代器所屬位置上,用value來構造一個T1類型的迭代器。spa

這裏值得要注意的是,容器對其中存儲元素的管理都是在堆上的(用自由存儲區更精確),也就是說容器自己多是在棧上,但它管理的元素必定是在堆上。3d

在回到上面的過程:指針

首先"string"這是一個char*指向的區域,而push_back調用的實際上裏面是一個string&變量,所以首先進行隱式轉換,調用string的string(const char*)這個含參的構造函數。

而這個生成的變量其實上是一個臨時變量。

push_back能夠看到他是傳參的,所以這個臨時變量直接被傳進去而沒有被拷貝構造。

到目前爲止,使用了隱式轉換,而此次隱式轉換調用了含參(char*)的string的構造函數。

進入以後進入construct,而construct也是傳引用,所以這裏也沒有調用構造。

可是在construct內部,在new時,使用了T1的含參構造,至關於在堆上利用這個臨時變量構造了一個T1類型的變量

在這裏調用了一次構造。

而因爲vector的迭代器是原始指針,所以這裏T1與T2是同一種類型(固然對其餘容器就不必定是這樣了)

於是調用的其實是拷貝構造

在堆上構造完畢後,construct退棧,push_back退棧,而後這個臨時變量由於離開做用域而被析構

實際上這個過程經歷了一次含參構造,一次拷貝構造,一次析構

std::vector<string> v;
v.push_back("string");

  

移動語義:

上面程序操做的問題癥結在於,臨時對象的構造和析構帶來了沒必要要的資源拷貝。若是有一種機制,能夠在語法層面識別出臨時對象,在使用臨時對象構造新對象(拷貝構造)的時候,將臨時對象所持有的資源『轉移』到新的對象中,就能消除這種沒必要要的拷貝(將「string」直接轉移給construct構造處的新對象)。這種語法機制就是『右值引用』,相對地,傳統的引用被稱爲『左值引用』。左值引用使用 ‘&’ 標識(好比 string&),右值引用使用 ‘&&’ 標識(好比 string&&)。順帶提一下什麼是左值(lvalue)什麼是(rvalue):能夠取地址的具名對象是左值;沒法取值的對象是右值,包括匿名的臨時對象和全部字面值(literal value)。有了右值的語法支持,爲了實現移動語義,須要相應類以右值爲參數重載傳統的拷貝構造函數和賦值操做符,畢竟哪些資源能夠移動、哪些只能拷貝只有類的實現者才知道。對於移動語義的拷貝『構造』,通常流程是將源對象的資源綁定到目的對象,而後解除源對象對資源的綁定;對於賦值操做,通常流程是,首先銷燬目的對象所持有的資源,而後改變資源的綁定。另外,固然,與傳統的構造和賦值類似,還要考慮到構造的異常安全和自賦值狀況。做爲演示:

其中,重載了String的"=",使遇到使用右值來進行構造時,轉移對象的資源。

class String {
public:
    String(const String &rhs) { ... }
    String(String &&rhs) {
        s_ = rhs.s_;
        rhs.s_ = NULL;
    }
    String& operator=(const String &rhs) { ... }
    String& operator=(String &&rhs) {
        if (this != &rhs) {
            delete [] s_;
            s_ = rhs.s_;
            rhs.s_ = NULL;
        }
        return *this;
    }
private:
    char *s_;
};

  值得注意的是,一個綁定到右值的右值引用是『左值』,由於它是有名字的。考慮:

其中,D的構造函數重載了一個右值引用版本,在這個版本中使用了初始化列表來初始化基類,但B(rhs)中rhs其實是個左值,也就是說對B調用構造時將會調用拷貝構造版本,而不是B的移動構造版本

class B {
public:
    B(const B&) {}
    B(B&&) {}
};
class D : public B {
    D(const D &rhs) : B(rhs) {}
    D(D &&rhs) : B(rhs) {}
};
D getD();
D d(getD());

  上面程序中,B::B(B&&) 不會被調用。爲此,C++11 中引入 std::move(T&& t) 模板函數,它 t 轉換爲右值:

class D : public B {
    D(D &&rhs) : B(std::move(rhs)) {}
};

  

綁定規則

引入右值引用後,『引用』到『值』的綁定規則也獲得擴充:

  • 左值引用能夠綁定到左值: int x; int &xr = x;
  • 很是量左值引用不能夠綁定到右值: int &r = 0;
  • 常量左值引用能夠綁定到左值和右值:int x; const int &cxr = x; const int &cr = 0;
  • 右值引用能夠綁定到右值:int &&r = 0;
  • 右值引用不能夠綁定到左值int x; int &&xr = x;
  • 常量右值引用沒有現實意義(畢竟右值引用的初衷在於移動語義,而移動就意味着『修改』)。

考慮下面這段代碼:

當調用不一樣的構造函數時,會輸出不一樣的語句

class HasPtrMem{
public:
	HasPtrMem():d(new int(3))
	{
		cout<<"Construct:"<<++n_cstr<<endl;
	}
	HasPtrMem(const HasPtrMem& tmp):d(new int(3))
	{
		cout<<"copy constructor:"<<++n_cptr<<endl;
	}
	HasPtrMem(HasPtrMem&& h)
	{
		d=h.d;
		h.d=nullptr;
		cout<<"move constructor:"<<++n_mvtr<<endl;
	}
	~HasPtrMem()
	{
		delete d;
		cout<<"destruct:"<<++n_dstr<<endl;
	}
	int * d;
	static int n_cstr;
	static int n_dstr;
	static int n_cptr;
	static int n_mvtr;
};
int HasPtrMem::n_cstr=0;
int HasPtrMem::n_dstr=0;
int HasPtrMem::n_cptr=0;
int HasPtrMem::n_mvtr=0;

HasPtrMem GetTemp()
{
	HasPtrMem n;
	return n;
}
int main() {
	
	HasPtrMem a=GetTemp();//a
     //HasPtrMem a;//b1
     //a=GetTemp;//b2
     //GetTemp//c
   cout<<endl; }

  咱們分別執行a,b1與b2和c:

執行a時:

分析一下這個過程,首先進入GetTemp,生成一個n,此處調用默認構造函數

而後返回n,在返回n的時候,理論上應該生成一個臨時變量,此處應該是編譯器優化過了,將a當作這個臨時變量直接進來構造。

而此處是調用的移動構造函數,儘管n是一個左值,難道說對臨時變量的構造都是調用的移動構造函數嗎???

構造完畢後,析構掉這個n

 

 執行b1,b2時:

分析一下這個過程,首先建立a,此時調用默認構造函數。

而後進入GetTemp內部,首先建立n,此處調用默認構造函數。

因爲咱們這裏是使用的賦值函數,所以不會像上面同樣進行優化。

首先用n移動構造一個臨時變量,構造完畢後n析構。

而後用這個臨時變量複製給a,因爲咱們沒有重載=,所以此處用的默認的賦值函數。

最後臨時變量析構

 

 執行c時:

咱們分析一下這個過程

注意這裏與a的區別,因爲這裏只調用了GetTemp,所以編譯期是沒法優化的。

首先建立n,調用默認構造函數

而後調用移動構造去構造臨時變量

而後n析構

最後臨時變量析構

 對於b1,b2,當咱們加上移動賦值函數後:

HasPtrMem& operator=(HasPtrMem&& s)
{
	d=s.d;
	s.d=nullptr;
	cout<<"move operator=:"<<endl;
	return *this;
}

結果爲:

咱們分析這個過程,前面不表。

在臨時變量調用移動構造函數構造完畢後,n析構。

而後對a,調用了移動賦值函數,而後臨時變量析構。

 

 

 看完上面,有一個問題是:

難道對臨時變量的構造必定使用移動構造函數嗎?這裏的n是一個左值!

相關文章
相關標籤/搜索