程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++ 11 右值引用以及std::move

C++ 11 右值引用以及std::move

編輯:關於C++

 

 

 

新類型:

 

int和int&是什麼?都是類型。int是整數類型,int&則是整數引用類型。同樣int&&也是一個類型。兩個引號&&是C++ 11提出的一個新的引用類型。記住,這是一個新的類型。默念10次吧。如果你記住這個新類型,那麼很多疑問都能迎刃而解。並且對《Effective Modern C++》說到的void f(Widget&& w),就很容易明白w是新類型的一個值,肯定是一個左值而不是右值,自然就不用去翻第二頁了。

出現了新類型,就像定義一個新類一樣,自然有兩件事接著要做:如何初始化、函數匹配(根據參數類型匹配函數)。先看後者。

 

void fun(int &a)
{
    cout<

 

main函數中的fun(a)會匹配第一個fun函數。因為第二個fun的參數是int右值引用,不能匹配一個左值。值得注意的是,雖然第二個fun函數的a的類型是右值引用類型,但它卻是一個左值,因為它是某一個類型變量嘛。

那要怎麼做才能使得b匹配第二個fun函數呢?強制類型轉換,把b強制轉換成右值引用類型,也就是使用static_cast(b)。此時,自然就會匹配第二個fun函數了。

在C++ 11中,static_cast有一個高大上的替代物std::move。其實,高大上的std::move做的事情和前面說的差不多,強制類型轉換使得匹配特定的函數而已。

右值引用和std::move引以自豪的高效率又是怎麼實現的呢?本文從經典的拷貝構造函數說起,但例子卻不經典。

 

class Test
{
public:
    Test() : p(nullptr) {}
    ~Test() { delete [] p; }

    Test(Test &t) : p(t.p)//注意這個拷貝構造函數的參數沒有const
    {
        t.p = nullptr;//不然會在析構函數中,delete兩次p
    }

private:
    char *p;
};

int main()
{
    Test a;
    Test b(a);
    return 0;
}

 

注意這個拷貝構造函數的參數沒有const。

讀者們,你們會覺得上面那個Test在拷貝構造函數不高效嗎?幾乎是沒有任何效率上的負擔啊。類似,也能寫一個高效的賦值函數。

但是,一般來說我們的拷貝構造函數的參數都是有const的。有const意味著不能修改參數t,上面的代碼也可以看到:將t.p賦值nullptr是必須的。因為t.p不能修改,所以不得不進行深復制,不然將出現經典的淺復制問題。不用說,有const的拷貝構造函數更適合一些,畢竟我們需要從一個const對象中復制一份。

 

移動構造:

 

性能的救贖:

在C++ 11之前,我們只能眼睜睜看著重量級的類只能調用有const的拷貝構造函數,復制一個重量級對象。在C++ 11裡面加入了一個新類型右值引用,那能不能用這個右值引用類型作為構造函數的參數呢?當然可以啦。畢竟類的構造函數參數沒有什麼特別的要求。習慣上,我們會稱這樣的構造函數為移動(move)構造函數,對應的賦值操作則稱為移動(move)賦值函數。他們的代碼也很簡單,如下:

 

class Test
{
public:
    Test() : p(nullptr)
    {
        cout<

 

 

協助完成移動構造:

有了move構造函數和move賦值函數,下一步是協助完成移動構造/移動賦值,包括程序員和編譯器。如果不協助的話,可能調用的是copy構造函數而不是move構造函數。從前文也可以看到,協助完成移動構造/移動賦值,其實也就是使得在函數調用時能匹配參數為右值引用的函數。碼農能做的就是強制將一個不需要了的對象調用std::move。如下面代碼:

 

int main()
{
	Test a;
	Test b = std::move(a);//調用move構造函數
	Test c = a;//調用copy構造函數
	return 0;
}

 

雖然上面的代碼在構造b的時候調用了移動構造,但明顯上面代碼一點都不正常,為什麼不直接構造b呢?完全用不著move構造啊。此時可能有讀者會想到這樣一個用途:我們可以為一個臨時對象加上std::move啊,比如operator + 的返回值。實際上這是畫蛇添足的。因為編譯器會為這個臨時對象當作右值(准確說應該是:將亡值),當然也就自動能使用移動構造了。

難道移動構造是屠龍之技?不是的。移動構造的一大優點是可以高效地在函數中返回一個重量級的類,函數返回值會在後面說到。除了在函數返回值用到外,在函數內部也可以使用到的。

 

std::vector g_ids;//全局變量
void addIds(std::string id)
{
    g_ids.push_back(std::move(id));
}


int main()
{
    addIds(1234);//在添加到g_ids過程中,會調用一次copy構造函數,一次move構造函數
    std::string my_id = 123456789;
    addIds(my_id);//會調用一次copy構造函數,一次move構造函數

    for(auto &e : g_ids)
        cout<

 

有讀者可能會問,為什麼addIds的參數不是const std::string &的形式,這樣在對my_id調用的時候就不用為參數id調用一次copy構造函數。但別忘了,此時id被push進g_ids時就要必須要調用一次copy構造函數了。

前面用紅色標出,對一個不需要的了對象調用std::move強制類型轉換。為什麼說是不需要了的呢?因為一個對象被std::move並且作為move構造函數的參數後,該對象所占用的一些資源可能被移走了,留下一個沒有用的空殼。注意,雖然是空殼,但在移動的時候,也要保證這個空殼對象能正確析構。

或許讀者還是覺得移動語義是屠龍之技,那麼讀者們想一下:vector容器在擴容的時候吧。有了移動語義,vector裡面的對象從舊地址搬到新地址,毫不費勁。

 

 

右值引用情況下的返回值問題:

 

有了右值引用,讀者可能會寫出下面的代碼:

 

Test&& fun()
{
    Test t;
    ...
    return std::move(t);
}


int main()
{
    Test && tt = fun();//和下者,哪個才是正確的呢?
    Test tt = fun();//和上者,哪個才是正確的呢?

    return 0;
}

 

無疑,在main函數中,還需要考慮一下tt對象是一個Test類型還是Test&&類型。其實,大錯早就在fun函數中鑄成了。

返回的只是一個引用,真身呢?真身已經在fun函數中被摧毀了。Meyers早在《Effective C++》裡面就告誡過:不要在函數中返回一個引用。前文也已經說了,右值引用也是一個引用(類型)! 那返回什麼好呢? 當然是真身啦! 如同下面代碼:

Test fun()
{
    Test t;
    ...

    return t;
}


int main()
{
	Test tt = fun();
	return 0;
}

當函數返回一個對象時,編譯器會將這個對象看作的一個右值(准確來說是將亡值)。所以無需在fun函數中,將return t寫成return std::move(t);

當然,實際上t變量的真身還是在fun函數中被摧毀了,但真身裡面有價值的東西都被移走了。對!就像比克大魔王那樣,臨死前把自己的孩子留下來! 在C++裡面,當然不能生成一個孩子,但是可以通過移動構造函數生成一個臨時對象,把有價值的東西移走。因為不是移動到main函數的tt變量中,只是移動到了臨時對象。所以接下來臨時對象還要進行一次移動,把有價值的東西移動到main函數的tt變量中。這個移動過程無疑是一個很好的金蟬脫殼的經典教程。讀者可以運行一下代碼,可以看到整個移動過程。記住,用g++編譯的時候要加入-fno-elide-constructors選項,禁止編譯器使用RVO優化。因為這裡的RVO優化比移動構造還是省力。所以如果不禁用,會優先使用RVO,而非移動構造函數。


初始化:

 

因為右值引用也是一個引用類型,所以只能初始化而不能賦值。既然這樣,那只需討論什麼類型的值才能用於初始化一個右值引用。一般來說,右值引用只能引用右值、字面值、將亡值。所以問題轉化為:什麼是右值?網上介紹的一個方法是:要能不能將取地址符號&應用於某個標識符,如果能就說明它是一個左值,否則為右值。這個方法好像是行得通的。不過,我覺得沒有必要分得那麼清楚,又不是在考試。在平常寫代碼時,沒有誰會寫類似a+++++a這樣的考試代碼。我個人覺得,記住最常見的那幾種就差不多了。比如,字面量(1,‘c'這類),臨時(匿名)對象(即將亡值),經過std::move()轉換的對象,函數返回值。其他的右值,還是留給編譯器和Scott Meyers吧。如果真的要細究,可以參考stackoverflow上的一個提問《What are rvalues, lvalues, xvalues, glvalues, and prvalues?》

還有一個問題需要說明,const的左值引用(const T&)是一個萬能引用,既可以引用左值,也能引用右值。這個是很特殊,特殊得很自然。如果Test類沒有定義move構造函數,但用戶又使用Test a = std::move(b)構造變量a。那麼最終會調用Test類的copy構造函數。一個類的copy構造函數如果用戶不定義,編譯器會在必要情況下自動合成一個。所以上面的a變量肯定能構造。


謹慎的編譯器:

 

前一段貌似隱隱約約說到編譯器不會自動合成一個move構造函數。是的。如果用戶定義了構造函數,copy構造函數,析構函數,operator =中的任何一個,編譯器都不會自動為這個類合成一個move構成函數以及move 賦值函數,即使需要用到。我個人覺得是因為,當定義了那四個函數中的任何一個,都可以認為這個類不是nontrival的了。

想一下,在什麼情況下我們是需要析構函數和copy構造函數的。當這個類裡面有一些資源(變量)需要我們手動管理的時候。既然有資源要管理,那麼讀者你覺得編譯器默認生成的move構造函數的內部實現應該是怎麼樣的呢?對類裡面的所有成員都調用std::move進行移動?還是調用copy構造函數復制一份呢?這種吃力但又不見得討好的事情,編譯器選擇不干。畢竟還有前面說到的const T& 可以引用一個右值。沒有move構造函數,copy構造函數頂上即可。

作為類的設計者,你當然知道那些資源(變量)到底是move還是copy。如果是move的話,那麼直接用=default告訴編譯器:別擔心,直接用對所有變量move就行了。如下:

 

class Test
{
public:
	Test() p(new int) {}
	~Test()=default;
	Test(const Test&)=delete;
	Test& operator = (const Test&)=delete;

	Test(Test &&)=default;//告訴編譯器
	Test& operator = (Test &&)=default;//告訴編譯器
	

private:
	std::unique_ptr p;
}


 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved