日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

【 C++11 】右值引用和移动语义

發布時間:2024/3/26 c/c++ 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【 C++11 】右值引用和移动语义 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

1、基本概念

????????左值 vs 左值引用

????????右值 vs 右值引用

????????左值引用 vs 右值引用

2、右值引用使用場景和意義

????????左值引用的使用場景

????????左值引用的短板

????????右值引用和移動語義

????????????①、移動構造

????????????②、移動賦值

????????????③、編譯器做的優化

????????????④、總結

????????右值引用引用左值

????????右值引用的其它場景(插入接口)

3、完美轉發

????????萬能引用&&

????????forward完美轉發在傳參的過程中保留對象原生類型屬性

????????完美轉發的使用場景


1、基本概念

傳統的C++語法中就有引用的語法,而C++11中新增了的右值引用語法特性,所以從現在開始我們之前學習的引用就叫做左值引用。無論左值引用還是右值引用,都是給對象取別名。


左值 vs 左值引用

左值:

左值是一個表示數據的表達式(如變量名或解引用的指針),有如下特性:

  • 我們可以獲取它的地址+可以對它賦值不一定能賦值,但一定能取地址
  • 左值可以出現賦值符號的左邊,右值不能出現在賦值符號左邊。
  • 定義時const修飾符后的左值不能給他賦值但是可以取它的地址
  • 左值引用:

    • 左值引用就是給左值的引用,給左值取別名
    int main() {// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下幾個是對上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0; }

    右值 vs 右值引用

    右值:

    右值也是一個表示數據的表達式,如臨時變量:字面常量、表達式返回值,函數返回值(這個不能是左值引用返回,要是傳值返回)等等,有如下特性:

  • 右值可以出現在賦值符號的右邊,但是不能出現出現在賦值符號的左邊,
  • 右值不能取地址
    • 綜上左值和右值最大區別在于左值可以取地址,右值不可以取地址(因為右值是臨時變量,沒有實際被存儲起來。

    補充:

    C++里又把右值分為兩類(純右值和將亡值):

  • 純右值內置類型的對象:10、a + b……
  • 將亡值自定義類型的對象:傳值返回生成的拷貝:to_string(1234)、匿名對象:string("11111")、s1 + "hello"
  • 右值引用:

    • 右值引用就是對右值的引用,給右值取別名
    int main() {double x = 1.1, y = 2.2;// 以下幾個都是常見的右值10;//字面常量x + y;//表達式返回值fmin(x, y);//函數返回值(傳值返回)// 以下幾個都是對右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);/*這里編譯會報錯:error C2106: “=”: 左操作數必須為左值10 = 1; x + y = 1; fmin(x, y) = 1;*//*這里編譯會報錯,右值不能取地址cout << &10 << endl;cout << &(x + y) << endl;cout << &fmin(x, y) << endl;*/return 0; }

    注意:

    • 右值是不能取地址的,但是給右值取別名后會導致右值被存儲到特定位置且可以取到該位置的地址,也就是說例如:不能取字面量10的地址,但是rr1引用后,可以對rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感覺很神奇,這個了解一下實際中右值引用的使用場景并不在于此,這個特性也不重要。
    int main() {double x = 1.1, y = 2.2;int&& rr1 = 10;const double&& rr2 = x + y;rr1 = 20;rr2 = 5.5; // 報錯return 0; }

    左值引用 vs 右值引用

    左值引用總結:

  • 左值引用只能引用左值,不能引用右值
  • 但是const左值引用既可以應用左值也可以引用右值
  • int main() {// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a; // ra為a的別名//int& ra2 = 10; // 編譯失敗,因為10是右值// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10;const int& ra4 = a;return 0; }

    右值引用總結:

  • 右值引用只能引用右值,不能引用左值
  • 但是右值引用可以引用move以后的左值
  • int main() {// 右值引用只能右值,不能引用左值。int&& r1 = 10;int a = 10;/*error C2440: “初始化”: 無法從“int”轉換為“int &&”message : 無法將左值綁定到右值引用int&& r2 = a;*/// 右值引用可以引用move以后的左值int&& r3 = move(a);return 0; }

    再次強調這四句話:

  • 左值引用只能引用左值,不能引用右值
  • 但是const左值引用既可以引用左值,也可以引用右值
  • 右值引用只能引用右值,不能引用左值
  • 但是右值引用可以引用move以后的左值
  • 右值引用是通過移動構造和移動賦值來極大提高深拷貝的效率,詳情見下文:


    2、右值引用使用場景和意義

    前面我們可以看到和const左值引用既可以引用左值又可以引用右值,那為什么C++11還要提出右值引用呢?是不是化蛇添足呢?下面我們來看看左值引用的使用場景與短板(深拷貝的問題),以及右值引用是如何補齊這個短板的!

    • 為了把整個過程說的通俗易懂,需要一個深拷貝的類,我們以先前模擬實現的string類來作為示例:簡易版string類的模擬實現

    左值引用的使用場景

    左值引用解決的是拷貝構造引發的深拷貝而帶來的開銷過大、效率低的問題:

    • 左值引用做參數,防止傳值傳參引發的拷貝構造問題(導致效率低)
    • 左值引用做返回值,防止返回對象發生拷貝構造的操作(導致效率低)
    void func1(cpp::string s) {} void func2(const cpp::string& s) {} int main() {cpp::string s1("hello");func1(s1);//值傳參func2(s1);//傳引用傳參// string operator+=(char ch) 傳值返回存在深拷貝// string& operator+=(char ch) 傳左值引用沒有拷貝提高了效率s1 += 'a';//左值引用作為返回值return 0; }

    總結:

    • 我們都清楚string類的+=運算符是左值引用作為返回值,這樣做避免了傳值返回引發的拷貝構造,而這樣做的原因在于string類的拷貝構造為深拷貝,要經歷開空間等操作,開銷太大了,導致效率低,傳值傳參同樣也是會發生拷貝構造(深拷貝)這個問題,為了避免如此之大的開銷,使用左值引用可以很好的解決此問題,因為左值引用就是取別名,無開銷,提高了效率

    左值引用的短板

    左值引用可以避免一些不必要的拷貝構造操作,但是并不是所有情況都是可以避免的:

    • 左值引用做參數,能夠完全避免傳參時不必要的拷貝操作
    • 左值引用做返回值,并不能完全避免函數返回對象時不必要的拷貝操作

    當函數返回的是一個臨時對象時,不能使用引用返回,因為臨時對象出了函數作用域就銷毀了,只能使用傳值返回,而傳值返回難免會引發拷貝構造帶來的深拷貝問題,但是無法避免,這就是左值引用的短板,示例:

    namesapce cpp {cpp::string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}cpp::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;} }

    因為這里的to_string是傳值返回,所以在調用to_string的時候一定會調用拷貝構造,而拷貝構造實現的又是一個深拷貝,效率低:

    int main() {cpp::string ret = cpp::to_string(1234);//string(const string& s) -- 深拷貝return 0; }

    解釋此情況不能用左值引用返回的原因:

    • 如果強硬的把上面的to_string實現成左值引用返回,那么又會出現一個問題,我str是臨時對象,因為是左值引用返回,所以返回的是str的別名,把別名作為返回值再區拷貝構造ret對象,但是臨時對象str出了作用域就調用析構函數銷毀了即使能夠訪問對象的值,但是空間已經不存在了,此時就發生了內存錯誤

    綜上所述,為了解決左值引用的短板,C++11引出了右值引用,但并不是簡單的把右值引用作為返回值,要對string進行改造,詳情見下文:


    右值引用和移動語義

    移動構造:

    • string拷貝構造的const左值引用會接收左值和右值,但是編譯器遵循最匹配原則,如果我們單獨增加一個右值引用版本的拷貝構造函數,使其只能接收右值,根據最匹配原則,遇到右值,傳入右值引用版本的拷貝構造函數,遇到左值傳入左值引用版本的拷貝構造函數,這樣就能解決了左值引用帶來的弊端,而上述單獨增加的函數就是我們的移動構造!!!

    移動賦值:

    • operator=函數采用的是const左值引用接收參數,因此無論賦值時傳入的是左值還是右值,都會調用原有的operator=函數。增加移動賦值之后,由于移動賦值采用的是右值引用接收參數,因此如果賦值時傳入的是右值,那么就會調用移動賦值函數(最匹配原則)。string原有的operator=函數做的是深拷貝,而移動賦值函數中只需要調用swap函數進行資源的轉移,因此調用移動賦值的代價比調用原有operator=的代價小。

    ①、移動構造

    為了解決左值引用的短板,我們需要在cpp::string中增加移動構造移動構造的本質是將參數右值將亡值的資源竊取過來,占位已有,那么就不用做深拷貝了,所以它叫做移動構造,就是竊取別人的資源來構造自己。因為將亡值的特點就是很快就要被銷毀了,在你銷毀之前還不如把你的資源通過移動構造傳給別人,也算是物盡其用,積善德了。

    • 該移動構造函數要做的就是調用swap函數將傳入右值的資源竊取過來,為了能夠更好的得知移動構造函數是否被調用,可以在該函數當中打印一條提示語句。
    namespace cpp {class string{public://移動構造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移動構造,資源轉移" << endl;swap(s);}private:char* _str;size_t _size;size_t _capacity;}; }

    測試代碼如下:

    int main() {cpp::string ret = cpp::to_string(1234);//轉移將亡值的資源cpp::string s1("hello");cpp::string s2(s1);//深拷貝,左值拷貝時不會被資源轉移cpp::string s3(move(s1));//轉移將亡值的資源return 0; }

    再來區分下移動構造和拷貝構造:

  • 在沒有添加移動構造之前,拷貝構造采用的是const左值引用接收參數,所以無論左值還是右值都會被傳進去,勢必會引發一系列左值引用的短板
  • 添加移動構造后,由于移動構造采用右值引用接收參數,只能接收右值
  • 根據編譯器的最匹配原則,左值傳入左值引用的拷貝構造,右值傳入右值引用的移動構造

  • ②、移動賦值

    移動賦值是一個賦值運算符重載函數,該函數的參數是右值引用類型的,移動賦值也是將傳入右值的資源竊取過來,占為己有,這樣就避免了深拷貝,所以它叫移動賦值,就是竊取別人的資源來賦值給自己的意思

    • 在當前的string類中增加一個移動賦值函數,該函數要做的就是調用swap函數將傳入右值的資源竊取過來,為了能夠更好的得知移動賦值函數是否被調用,可以在該函數中打印一條提示語句。
    namespace cpp {class string{public:// 移動賦值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移動賦值" << endl;swap(s);return *this;}private:char* _str;size_t _size;size_t _capacity;}; }

    來區分下移動賦值operator=

  • 在沒有增加移動賦值之前,由于原有operator=函數采用的是const左值引用接收參數,因此無論賦值時傳入的是左值還是右值,都會調用原有的operator=函數。
  • 增加移動賦值之后,由于移動賦值采用的是右值引用接收參數,因此如果賦值時傳入的是右值,那么就會調用移動賦值函數(最匹配原則)。
  • string原有的operator=函數做的是深拷貝,而移動賦值函數中只需要調用swap函數進行資源的轉移,因此調用移動賦值的代價比調用原有operator=的代價小。
  • 測試代碼如下:

    int main() {cpp::string ret;//string(string&& s) -- 移動構造,資源轉移ret = cpp::to_string(1234);//string& operator=(string&& s) -- 移動賦值,資源轉換return 0; }

    總結:

    • 這里運行后,我們看到調用了一次移動構造和一次移動賦值。因為如果是用一個已經存在的對象接收,編譯器就沒辦法優化了。cpp::to_string函數中會先用str生成構造生成一個臨時對象,但是我們可以看到,編譯器很聰明的在這里把str識別成了右值,調用了移動構造。然后在把這個臨時對象做為cpp::to_string函數調用的返回值賦值給ret1,這里調用的移動賦值。
    • 這里雖然調用兩次函數,但都只是資源的移動,不需要進行深拷貝,大大提高了效率。

    ③、編譯器做的優化

    以如下代碼為測試案例:

    int main() {cpp::string s = cpp::to_string(1234);return 0; }

    3.1、先來看下沒有移動構造編譯器做的優化:

    不優化:

    • 如果沒有移動構造,那我們先前實現的to_string只能夠傳值返回,傳值返回會先拷貝構造出一個臨時對象,再用這個臨時對象再拷貝構造我們接收返回值的對象。如圖所示:

    優化:

    • C++11標準出來之前,也就是C++98的情況,本來應該是兩次拷貝構造,但是編譯器對其進行了優化,連續兩次的拷貝構造函數最終被優化成一次,直接拿str拷貝構造s。

    3.2、再來看看有移動構造編譯器做的優化:

    不優化:

    • C++11出來后,我們假設它不優化,根據先前的了解,不優化的話,左值str會拷貝構造給一個臨時對象,這個臨時對象就是一個右值(將亡值),隨后進行移動構造,也就是先拷貝構造再移動構造:

    優化:?

    • C++11這里編譯器進行優化后,左值str會被優化成右值(通過move把左值變為右值),再移動構造給一個臨時對象,此臨時對象再移動構造給s,但是編譯器還會再進行一次優化,把左值str識別出右值后直接移動構造給s。也就是只進行一次移動構造

    3.3、來看看編譯器對移動賦值的處理:

    • 當我們不是用函數的返回值來構造一個對象,而是用一個之前已經定義出來的對象來接收函數的返回值,測試代碼如下:
    int main() {cpp::string ret;ret = cpp::to_string(1234);return 0; }

    此時編譯器會把左值str會被優化成右值(通過move把左值變為右值),再移動構造給一個臨時對象,此臨時對象再通過移動賦值傳給之前已經定義出來的對象。

    這里編譯器并沒有對這種情況進行優化,因為如果是用一個已經存在的對象接收,編譯器就沒辦法優化了。cpp::to_string函數中會先用str生成構造生成一個臨時對象,但是我們可以看到,編譯器很聰明的在這里把str識別成了右值,調用了移動構造。然后在把這個臨時對象做為cpp::to_string函數調用的返回值賦值給ret1,這里調用的移動賦值。


    ④、總結

  • 左值引用的深拷貝 -- 拷貝構造 / 拷貝賦值
  • 右值引用的深拷貝 -- 移動構造 / 移動賦值
  • C++11后STL中的容器都是增加了移動構造和移動賦值:

    • https://cplusplus.com/reference/string/string/string/
    • http://www.cplusplus.com/reference/vector/vector/vector/

    右值引用引用左值

    這部分內容我在一開始簡要提了一下,下面來正式了解下move函數:

    • 按照語法,右值引用只能引用右值,但右值引用一定不能引用左值嗎?因為:有些場景下,可能真的需要用右值去引用左值實現移動語義。當需要用右值引用引用一個左值時,可以通過move函數將左值轉化為右值。C++11中,std::move()函數位于<utility>頭文件中,該函數名字具有迷惑性,它并不搬移任何東西,唯一的功能就是將一個左值強制轉化為右值引用,然后實現移動語義。

    來看下move函數的定義:

    template<class _Ty> inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT {// forward _Arg as movablereturn ((typename remove_reference<_Ty>::type&&)_Arg); }

    注意:

    • move函數中_Arg參數的類型不是右值引用,而是萬能引用。萬能引用跟右值引用的形式一樣,但是右值引用需要是確定的類型。
    • 一個左值被move以后,它的資源可能就被轉移給別人了,因此要慎用一個被move后的左值。

    測試如下:

    int main() {cpp::string s1("hello world");// 這里s1是左值,調用的是拷貝構造cpp::string s2(s1);//string(const string& s) -- 深拷貝// 這里我們把s1 move處理以后, 會被當成右值,調用移動構造// 但是這里要注意,一般是不要這樣用的,因為我們會發現s1的// 資源被轉移給了s3,s1被置空了。cpp::string s3(std::move(s1));//string(string&& s) -- 移動構造return 0; }

    右值引用的其它場景(插入接口)

    C++11后STL容器中的插入接口函數也增加了右值引用的版本:

    注意:

    • C++98的時候,push_back函數只有const左值引用版本,所以這就會導致無論是左值還是右值都會被傳入這個左值引用版本的push_back,勢必會引發后續的深拷貝而帶來的開銷過大等問題。
    • C++11出來后,push_back函數增加了右值引用版本,如果傳入push_back函數的是一個右值,那么在push_back函數構造節點時,這個右值就可以匹配到容器的移動構造函數進行資源的轉移,這樣就避免了深拷貝,提高了效率。

    示例:

    int main() {list<cpp::string> lt;cpp::string s1("1111");// 這里調用的是拷貝構造lt.push_back(s1);//string(const string& s) -- 深拷貝// 下面調用都是移動構造5lt.push_back("2222");//string(string&& s) -- 移動構造lt.push_back(std::move(s1));//string(string&& s) -- 移動構造return 0; }

    上述代碼中的插入第一個元素s1就會匹配到push_back的左值引用版本,在push_back函數內部就會調用string的拷貝構造函數進行深拷貝,而后面插入的兩個元素時由于傳入的是右值,因此會匹配到push_back的右值引用版本,此時在push_back函數內部就會調用string的移動構造函數進行資源的轉移。


    3、完美轉發

    萬能引用&&

    &&應用在模板中時,不代表右值引用,而是萬能引用,萬能引用既能接收左值,也能接收右值。

    template<typename T> void PerfectForward(T&& t)//萬能引用 {//…… }

    萬能引用的作用:

  • 模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。
  • 模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力,
  • 但是引用類型的唯一作用就是限制了接收的類型后續使用中都退化成了左值
  • 示例:

    void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; } void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; } template<typename T> void PerfectForward(T&& t) {Fun(t); } int main() {PerfectForward(10);//右值int a;PerfectForward(a);//左值PerfectForward(std::move(a));//右值const int b = 8;PerfectForward(b);//const左值PerfectForward(std::move(b));//const右值return 0; }

    注意看上面的Fun函數我寫了四個,分別是左值引用、const左值引用、右值引用、const右值引用。main函數中我把左值、右值、const左值、const右值均作為參數傳入了函數模板PerfectForward里頭,因為其參數類型是萬能引用&&,所以既可以接收左值也可以接收右值,可是最終的測試結果令我們大跌眼鏡:

    • 實際傳入PerfectForward函數模板的左值和右值均匹配到了左值引用版本的Fun函數,而傳入PerfectForward函數模板的const左值和const右值均匹配到了const左值引用版本的Fun函數。
    • 造成此現象的根本原因在于右值被引用后會導致右值被存儲到特定位置,這時這個右值可以被取到地址,并且可以被修改,所以在PerfectForward函數中調用Func函數時會將t識別成左值

    這也就是我們上文所提到的萬能引用限制了接收的類型,在后續使用中均退化成了左值,但是我們希望能夠在傳遞過程中保持它的左值或者右值的屬性, 就需要用我們下面學習的完美轉發。


    forward完美轉發在傳參的過程中保留對象原生類型屬性

    我們想要在傳參的過程中保留對象的原生類型屬性,就需要用到forward函數:

    template<typename T> void PerfectForward(T&& t) {//完美轉發Fun(std::forward<T>(t));//std::forward<T>(t)在傳參的過程中保持了t的原生類型屬性。 }

    測試結果如下:

    完美轉發后,左值、右值、左值引用、右值引用就可以被傳入到理想狀態下的函數接口了。


    完美轉發的使用場景

    這里我們把先前模擬實現的list拖過來做測試案例,鏈接:list的模擬實現

    • 先前實現的list是沒有對push_back函數和insert函數寫一個右值引用版本的,所以這就會導致無論數據是左值還是右值都會傳入左值引用的版本,勢必在構建節點的時候引發深拷貝,測試代碼如下:
    int main() {cpp::list<cpp::string> lt;cpp::string s1("1111");//右值lt.push_back(s1);//左值lt.push_back("2222");//右值lt.push_back(std::move(s1));//右值 }

    為了避免深拷貝帶來的開銷過大,我們對push_back和insert函數單獨寫一個右值引用的版本,同樣也要對構造函數寫一個右值引用的版本,因為創建節點需要用到節點類的構造函數:

    //節點類 template<class T> struct list_node {//……//右值引用節點類構造函數list_node(T&& val):_next(nullptr), _prev(nullptr), _data(val){} }; template<class T> class list { public://……//右值引用版本的push_backvoid push_back(T&& xx){insert(end(), xx);}//右值引用版本的insertiterator insert(iterator pos, T&& xx){Node* newnode = new Node(xx);//創建新的結點Node* cur = pos._node; //迭代器pos處的結點指針Node* prev = cur->_prev;//prev newnode cur//鏈接prev和newnodeprev->_next = newnode;newnode->_prev = prev;//鏈接newnode和curnewnode->_next = cur;cur->_prev = newnode;//返回新插入元素的迭代器位置return iterator(newnode);} private:Node* _head; }

    雖然這里實現了右值引用版本,但是實際的運行結果依然是深拷貝的,和沒寫之前的運行結果一模一樣,原因如下:

    • 根據先前的了解我們得知:&&應用在模板中時,不代表右值引用,而是萬能引用,萬能引用既能接收左值,也能接收右值。但是在后續的使用中,會把接收的類型全部退化成左值,既然退化成左值,那么自然會進入后續的深拷貝。

    此情況就是典型的完美轉發的使用場景,解決辦法如下:

    • 我們需要在傳參的過程中保留對象的原生類型屬性,就需要用到forward函數:
    //右值引用節點類的構造函數 list_node(T&& val):_next(nullptr), _prev(nullptr), _data(std::forward<T>(val))//完美轉發 {} //右值引用版本的push_back void push_back(T&& xx) {//完美轉發insert(end(), std::forward<T>(xx)); } //右值引用版本的insert iterator insert(iterator pos, T&& xx) {//完美轉發Node* newnode = new Node(std::forward<T>(xx));//……return iterator(newnode); }

    修改好后再次運行,結果就是我們想要的啦:

    總結

    以上是生活随笔為你收集整理的【 C++11 】右值引用和移动语义的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。