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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > c/c++ >内容正文

c/c++

C++11 右值引用、移动语义、完美转发、万能引用

發(fā)布時(shí)間:2025/3/8 c/c++ 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++11 右值引用、移动语义、完美转发、万能引用 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

C++11 右值引用、移動(dòng)語(yǔ)義、完美轉(zhuǎn)發(fā)、引用折疊、萬(wàn)能引用

轉(zhuǎn)自:http://c.biancheng.net/

C++中的左值和右值

右值引用可以從字面意思上理解,指的是以引用傳遞(而非值傳遞)的方式使用 C++ 右值。關(guān)于 C++ 引用,已經(jīng)在《C++引用》專題給大家做了詳細(xì)的講解,這里不再重復(fù)贅述。接下來(lái)重點(diǎn)給大家介紹什么是 C++ 右值。

在 C++ 或者 C 語(yǔ)言中,一個(gè)表達(dá)式(可以是字面量、變量、對(duì)象、函數(shù)的返回值等)根據(jù)其使用場(chǎng)景不同,分為左值表達(dá)式和右值表達(dá)式。確切的說(shuō) C++ 中左值和右值的概念是從 C 語(yǔ)言繼承過(guò)來(lái)的。

值得一提的是,左值的英文簡(jiǎn)寫為“l(fā)value”,右值的英文簡(jiǎn)寫為“rvalue”。很多人認(rèn)為它們分別是"left value"、“right value” 的縮寫,其實(shí)不然。lvalue 是“l(fā)oactor value”的縮寫,可意為存儲(chǔ)在內(nèi)存中、有明確存儲(chǔ)地址(可尋址)的數(shù)據(jù),而 rvalue 譯為 “read value”,指的是那些可以提供數(shù)據(jù)值的數(shù)據(jù)(不一定可以尋址,例如存儲(chǔ)于寄存器中的數(shù)據(jù))。

通常情況下,判斷某個(gè)表達(dá)式是左值還是右值,最常用的有以下 2 種方法。

  • 可位于賦值號(hào)(=)左側(cè)的表達(dá)式就是左值;反之,只能位于賦值號(hào)右側(cè)的表達(dá)式就是右值。舉個(gè)例子:
  • int a = 5; 5 = a; //錯(cuò)誤,5 不能為左值

    其中,變量 a 就是一個(gè)左值,而字面量 5 就是一個(gè)右值。值得一提的是,C++ 中的左值也可以當(dāng)做右值使用,例如:

    int b = 10; // b 是一個(gè)左值 a = b; // a、b 都是左值,只不過(guò)將 b 可以當(dāng)做右值使用
  • 有名稱的、可以獲取到存儲(chǔ)地址的表達(dá)式即為左值;反之則是右值。
  • 以上面定義的變量 a、b 為例,a 和 b 是變量名,且通過(guò) &a 和 &b 可以獲得他們的存儲(chǔ)地址,因此 a 和 b 都是左值;反之,字面量 5、10,它們既沒有名稱,也無(wú)法獲取其存儲(chǔ)地址(字面量通常存儲(chǔ)在寄存器中,或者和代碼存儲(chǔ)在一起),因此 5、10 都是右值。

    注意,以上 2 種判定方法只適用于大部分場(chǎng)景。由于本節(jié)主要講解右值引用,因此這里適可而止,不再對(duì) C++ 左值和右值做深度剖析,感興趣的讀者可自行研究。

    更詳細(xì)的左右值的相關(guān)概念可參考:

    C++右值引用

    前面提到,其實(shí) C++98/03 標(biāo)準(zhǔn)中就有引用,使用 “&” 表示。但此種引用方式有一個(gè)缺陷,即正常情況下只能操作 C++ 中的左值,無(wú)法對(duì)右值添加引用。舉個(gè)例子:

    int num = 10;int &b = num; //正確int &c = 10; //錯(cuò)誤

    如上所示,編譯器允許我們?yōu)?num 左值建立一個(gè)引用,但不可以為 10 這個(gè)右值建立引用。因此,C++98/03 標(biāo)準(zhǔn)中的引用又稱為左值引用。

    注意,雖然 C++98/03 標(biāo)準(zhǔn)不支持為右值建立非常量左值引用,但允許使用常量左值引用操作右值。也就是說(shuō),常量左值引用既可以操作左值,也可以操作右值,例如:

    int num = 10;const int &b = num;const int &c = 10;

    我們知道,右值往往是沒有名稱的,因此要使用它只能借助引用的方式。這就產(chǎn)生一個(gè)問題,實(shí)際開發(fā)中我們可能需要對(duì)右值進(jìn)行修改(實(shí)現(xiàn)移動(dòng)語(yǔ)義時(shí)就需要),顯然左值引用的方式是行不通的。

    為此,C++11 標(biāo)準(zhǔn)新引入了另一種引用方式,稱為右值引用,用 “&&” 表示。

    話說(shuō),C++標(biāo)準(zhǔn)委員會(huì)在選定右值引用符號(hào)時(shí),既希望能選用現(xiàn)有 C++ 內(nèi)部已有的符號(hào),還不能與 C++ 98 /03 標(biāo)準(zhǔn)產(chǎn)生沖突,最終選定了 2 個(gè) ‘&’ 表示右值引用。

    需要注意的,和聲明左值引用一樣,右值引用也必須立即進(jìn)行初始化操作,且只能使用右值進(jìn)行初始化,比如:

    int num = 10;//int && a = num; //右值引用不能初始化為左值int && a = 10;

    和常量左值引用不同的是,右值引用還可以對(duì)右值進(jìn)行修改。例如:

    int && a = 10;a = 100;cout << a << endl;

    程序輸出結(jié)果為 100。

    另外值得一提的是,C++ 語(yǔ)法上是支持定義常量右值引用的,例如:

    const int&& a = 10;//編譯器不會(huì)報(bào)錯(cuò)

    但這種定義出來(lái)的右值引用并無(wú)實(shí)際用處。一方面,右值引用主要用于移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā),其中前者需要有修改右值的權(quán)限;其次,常量右值引用的作用就是引用一個(gè)不可修改的右值,這項(xiàng)工作完全可以交給常量左值引用完成。

    學(xué)到這里,一些讀者可能無(wú)法記清楚左值引用和右值引用各自可以引用左值還是右值,這里給大家一張表格,方便大家記憶:

    引用類型使用場(chǎng)景
    非常量左值常量左值非常量右值常量右值
    非常量左值引用YNNN無(wú)
    常量左值引用YYYY常用于類中構(gòu)建拷貝構(gòu)造函數(shù)
    非常量右值引用NNYN移動(dòng)語(yǔ)義、完美轉(zhuǎn)發(fā)
    常量右值引用NNYY無(wú)實(shí)際用途

    表中,Y 表示支持,N 表示不支持。

    其實(shí),C++11 標(biāo)準(zhǔn)中對(duì)右值做了更細(xì)致的劃分,分別稱為純右值(Pure value,簡(jiǎn)稱 pvalue)和將亡值(eXpiring value,簡(jiǎn)稱 xvalue )。其中純右值就是 C++98/03 標(biāo)準(zhǔn)中的右值(本節(jié)中已經(jīng)做了大篇幅的講解),而將亡值則指的是和右值引用相關(guān)的表達(dá)式(比如某函數(shù)返回的 T && 類型的表達(dá)式)。對(duì)于純右值和將亡值,都屬于右值,讀者知道即可,不必深究。

    拷貝構(gòu)造函數(shù)與深拷貝

    在 C++ 11 標(biāo)準(zhǔn)之前(C++ 98/03 標(biāo)準(zhǔn)中),如果想用其它對(duì)象初始化一個(gè)同類的新對(duì)象,只能借助類中的復(fù)制(拷貝)構(gòu)造函數(shù)。通過(guò)《C++拷貝構(gòu)造函數(shù)》一節(jié)的學(xué)習(xí)我們知道,拷貝構(gòu)造函數(shù)的實(shí)現(xiàn)原理很簡(jiǎn)單,就是為新對(duì)象復(fù)制一份和其它對(duì)象一模一樣的數(shù)據(jù)。

    需要注意的是,當(dāng)類中擁有指針類型的成員變量時(shí),拷貝構(gòu)造函數(shù)中需要以深拷貝(而非淺拷貝)的方式復(fù)制該指針成員。有關(guān)深拷貝和淺拷貝以及它們的區(qū)別,讀者可閱讀《C++深拷貝和淺拷貝》一文做詳細(xì)了解。

    舉個(gè)例子:

    #include <iostream> using namespace std;class demo{ public:demo():num(new int(0)){cout<<"construct!"<<endl;}//拷貝構(gòu)造函數(shù)demo(const demo &d):num(new int(*d.num)){cout<<"copy construct!"<<endl;}~demo(){cout<<"class destruct!"<<endl;} private:int *num; };demo get_demo(){return demo(); }int main(){demo a = get_demo();return 0; }

    如上所示,我們?yōu)?demo 類自定義了一個(gè)拷貝構(gòu)造函數(shù)。該函數(shù)在拷貝 d.num 指針成員時(shí),必須采用深拷貝的方式,即拷貝該指針成員本身的同時(shí),還要拷貝指針指向的內(nèi)存資源。否則一旦多個(gè)對(duì)象中的指針成員指向同一塊堆空間,這些對(duì)象析構(gòu)時(shí)就會(huì)對(duì)該空間釋放多次,這是不允許的。

    可以看到,程序中定義了一個(gè)可返回 demo 對(duì)象的 get_demo() 函數(shù),用于在 main() 主函數(shù)中初始化 a 對(duì)象,其整個(gè)初始化的流程包含以下幾個(gè)階段:

  • 執(zhí)行 get_demo() 函數(shù)內(nèi)部的 demo() 語(yǔ)句,即調(diào)用 demo 類的默認(rèn)構(gòu)造函數(shù)生成一個(gè)匿名對(duì)象;
  • 執(zhí)行 return demo() 語(yǔ)句,會(huì)調(diào)用拷貝構(gòu)造函數(shù)復(fù)制一份之前生成的匿名對(duì)象,并將其作為 get_demo() 函數(shù)的返回值(函數(shù)體執(zhí)行完畢之前,匿名對(duì)象會(huì)被析構(gòu)銷毀);
  • 執(zhí)行 a = get_demo() 語(yǔ)句,再調(diào)用一次拷貝構(gòu)造函數(shù),將之前拷貝得到的臨時(shí)對(duì)象復(fù)制給 a(此行代碼執(zhí)行完畢,get_demo() 函數(shù)返回的對(duì)象會(huì)被析構(gòu));
  • 程序執(zhí)行結(jié)束前,會(huì)自行調(diào)用 demo 類的析構(gòu)函數(shù)銷毀 a。
  • 注意,目前多數(shù)編譯器都會(huì)對(duì)程序中發(fā)生的拷貝操作進(jìn)行優(yōu)化,因此如果我們使用 VS 2017、codeblocks 等這些編譯器運(yùn)行此程序時(shí),看到的往往是優(yōu)化后的輸出結(jié)果:

    construct! class destruct!

    而同樣的程序,如果在 Linux 上使用g++ demo.cpp -fno-elide-constructors命令運(yùn)行(其中 demo.cpp 是程序文件的名稱),就可以看到完整的輸出結(jié)果:

    construct! <-- 執(zhí)行 demo() copy construct! <-- 執(zhí)行 return demo() class destruct! <-- 銷毀 demo() 產(chǎn)生的匿名對(duì)象 copy construct! <-- 執(zhí)行 a = get_demo() class destruct! <-- 銷毀 get_demo() 返回的臨時(shí)對(duì)象 class destruct! <-- 銷毀 a

    如上所示,利用拷貝構(gòu)造函數(shù)實(shí)現(xiàn)對(duì) a 對(duì)象的初始化,底層實(shí)際上進(jìn)行了 2 次拷貝(而且是深拷貝)操作。當(dāng)然,對(duì)于僅申請(qǐng)少量堆空間的臨時(shí)對(duì)象來(lái)說(shuō),深拷貝的執(zhí)行效率依舊可以接受,但如果臨時(shí)對(duì)象中的指針成員申請(qǐng)了大量的堆空間,那么 2 次深拷貝操作勢(shì)必會(huì)影響 a 對(duì)象初始化的執(zhí)行效率。

    事實(shí)上,此問題一直存留在以 C++ 98/03 標(biāo)準(zhǔn)編寫的 C++ 程序中。由于臨時(shí)變量的產(chǎn)生、銷毀以及發(fā)生的拷貝操作本身就是很隱晦的(編譯器對(duì)這些過(guò)程做了專門的優(yōu)化),且并不會(huì)影響程序的正確性,因此很少進(jìn)入程序員的視野。

    那么當(dāng)類中包含指針類型的成員變量,使用其它對(duì)象來(lái)初始化同類對(duì)象時(shí),怎樣才能避免深拷貝導(dǎo)致的效率問題呢?C++11 標(biāo)準(zhǔn)引入了解決方案,該標(biāo)準(zhǔn)中引入了右值引用的語(yǔ)法,借助它可以實(shí)現(xiàn)移動(dòng)語(yǔ)義。

    移動(dòng)構(gòu)造函數(shù)、移動(dòng)語(yǔ)義及其實(shí)現(xiàn)

    所謂移動(dòng)語(yǔ)義,指的就是以移動(dòng)而非深拷貝的方式初始化含有指針成員的類對(duì)象。簡(jiǎn)單的理解,移動(dòng)語(yǔ)義指的就是將其他對(duì)象(通常是臨時(shí)對(duì)象)擁有的內(nèi)存資源“移為已用”。

    以前面程序中的 demo 類為例,該類的成員都包含一個(gè)整形的指針成員,其默認(rèn)指向的是容納一個(gè)整形變量的堆空間。當(dāng)使用 get_demo() 函數(shù)返回的臨時(shí)對(duì)象初始化 a 時(shí),我們只需要將臨時(shí)對(duì)象的 num 指針直接淺拷貝給 a.num,然后修改該臨時(shí)對(duì)象中 num 指針的指向(通常另其指向 NULL),這樣就完成了 a.num 的初始化。

    事實(shí)上,對(duì)于程序執(zhí)行過(guò)程中產(chǎn)生的臨時(shí)對(duì)象,往往只用于傳遞數(shù)據(jù)(沒有其它的用處),并且會(huì)很快會(huì)被銷毀。因此在使用臨時(shí)對(duì)象初始化新對(duì)象時(shí),我們可以將其包含的指針成員指向的內(nèi)存資源直接移給新對(duì)象所有,無(wú)需再新拷貝一份,這大大提高了初始化的執(zhí)行效率。

    例如,下面程序?qū)?demo 類進(jìn)行了修改:

    #include <iostream> using namespace std; class demo{ public:demo():num(new int(0)){cout<<"construct!"<<endl;}demo(const demo &d):num(new int(*d.num)){cout<<"copy construct!"<<endl;}//添加移動(dòng)構(gòu)造函數(shù)demo(demo &&d):num(d.num){d.num = NULL;cout<<"move construct!"<<endl;}~demo(){cout<<"class destruct!"<<endl;} private:int *num; }; demo get_demo(){return demo(); } int main(){demo a = get_demo();return 0; }

    可以看到,在之前 demo 類的基礎(chǔ)上,我們又手動(dòng)為其添加了一個(gè)構(gòu)造函數(shù)。和其它構(gòu)造函數(shù)不同,此構(gòu)造函數(shù)使用右值引用形式的參數(shù),又稱為移動(dòng)構(gòu)造函數(shù)。并且在此構(gòu)造函數(shù)中,num 指針變量采用的是淺拷貝的復(fù)制方式,同時(shí)在函數(shù)內(nèi)部重置了 d.num,有效避免了“同一塊對(duì)空間被釋放多次”情況的發(fā)生。

    在 Linux 系統(tǒng)中使用g++ demo.cpp -o demo.exe -std=c++0x -fno-elide-constructors命令執(zhí)行此程序,輸出結(jié)果為:

    construct! move construct! class destruct! move construct! class destruct! class destruct!\

    通過(guò)執(zhí)行結(jié)果我們不難得知,當(dāng)為 demo 類添加移動(dòng)構(gòu)造函數(shù)之后,使用臨時(shí)對(duì)象初始化 a 對(duì)象過(guò)程中產(chǎn)生的 2 次拷貝操作,都轉(zhuǎn)由移動(dòng)構(gòu)造函數(shù)完成。

    我們知道,非 const 右值引用只能操作右值,程序執(zhí)行結(jié)果中產(chǎn)生的臨時(shí)對(duì)象(例如函數(shù)返回值、lambda 表達(dá)式等)既無(wú)名稱也無(wú)法獲取其存儲(chǔ)地址,所以屬于右值。當(dāng)類中同時(shí)包含拷貝構(gòu)造函數(shù)和移動(dòng)構(gòu)造函數(shù)時(shí),如果使用臨時(shí)對(duì)象初始化當(dāng)前類的對(duì)象,編譯器會(huì)優(yōu)先調(diào)用移動(dòng)構(gòu)造函數(shù)來(lái)完成此操作。只有當(dāng)類中沒有合適的移動(dòng)構(gòu)造函數(shù)時(shí),編譯器才會(huì)退而求其次,調(diào)用拷貝構(gòu)造函數(shù)。

    在實(shí)際開發(fā)中,通常在類中自定義移動(dòng)構(gòu)造函數(shù)的同時(shí),會(huì)再為其自定義一個(gè)適當(dāng)?shù)目截悩?gòu)造函數(shù),由此當(dāng)用戶利用右值初始化類對(duì)象時(shí),會(huì)調(diào)用移動(dòng)構(gòu)造函數(shù);使用左值(非右值)初始化類對(duì)象時(shí),會(huì)調(diào)用拷貝構(gòu)造函數(shù)。

    讀者可能會(huì)問,如果使用左值初始化同類對(duì)象,但也想調(diào)用移動(dòng)構(gòu)造函數(shù)完成,有沒有辦法可以實(shí)現(xiàn)呢?

    默認(rèn)情況下,左值初始化同類對(duì)象只能通過(guò)拷貝構(gòu)造函數(shù)完成,如果想調(diào)用移動(dòng)構(gòu)造函數(shù),則必須使用右值進(jìn)行初始化。C++11 標(biāo)準(zhǔn)中為了滿足用戶使用左值初始化同類對(duì)象時(shí)也通過(guò)移動(dòng)構(gòu)造函數(shù)完成的需求,新引入了 std::move() 函數(shù),它可以將左值強(qiáng)制轉(zhuǎn)換成對(duì)應(yīng)的右值,由此便可以使用移動(dòng)構(gòu)造函數(shù)。

    std::move()

    通過(guò)上節(jié)內(nèi)容,我們知道,C++11 標(biāo)準(zhǔn)中借助右值引用可以為指定類添加移動(dòng)構(gòu)造函數(shù),這樣當(dāng)使用該類的右值對(duì)象(可以理解為臨時(shí)對(duì)象)初始化同類對(duì)象時(shí),編譯器會(huì)優(yōu)先選擇移動(dòng)構(gòu)造函數(shù)。

    注意,移動(dòng)構(gòu)造函數(shù)的調(diào)用時(shí)機(jī)是:用同類的右值對(duì)象初始化新對(duì)象。那么,用當(dāng)前類的左值對(duì)象(有名稱,能獲取其存儲(chǔ)地址的實(shí)例對(duì)象)初始化同類對(duì)象時(shí),是否就無(wú)法調(diào)用移動(dòng)構(gòu)造函數(shù)了呢?當(dāng)然不是,C++11 標(biāo)準(zhǔn)中已經(jīng)給出了解決方案,即調(diào)用 move() 函數(shù)。

    move 本意為 “移動(dòng)”,但該函數(shù)并不能移動(dòng)任何數(shù)據(jù),它的功能很簡(jiǎn)單,就是將某個(gè)左值強(qiáng)制轉(zhuǎn)化為右值。

    基于 move() 函數(shù)特殊的功能,其常用于實(shí)現(xiàn)移動(dòng)語(yǔ)義。

    move() 函數(shù)的用法也很簡(jiǎn)單,其語(yǔ)法格式如下:

    move( arg )

    其中,arg 表示指定的左值對(duì)象。該函數(shù)會(huì)返回 arg 對(duì)象的右值形式。

    【例 1】move() 函數(shù)的基礎(chǔ)應(yīng)用。

    #include <iostream> using namespace std; class movedemo{ public:movedemo():num(new int(0)){cout<<"construct!"<<endl;}//拷貝構(gòu)造函數(shù)movedemo(const movedemo &d):num(new int(*d.num)){cout<<"copy construct!"<<endl;}//移動(dòng)構(gòu)造函數(shù)movedemo(movedemo &&d):num(d.num){d.num = NULL;cout<<"move construct!"<<endl;} public: //這里應(yīng)該是 private,使用 public 是為了更方便說(shuō)明問題int *num; }; int main(){movedemo demo;cout << "demo2:\n";movedemo demo2 = demo;//cout << *demo2.num << endl; //可以執(zhí)行cout << "demo3:\n";movedemo demo3 = std::move(demo);//此時(shí) demo.num = NULL,因此下面代碼會(huì)報(bào)運(yùn)行時(shí)錯(cuò)誤//cout << *demo.num << endl;return 0; }

    程序執(zhí)行結(jié)果為:

    construct! demo2: copy construct! demo3: move construct!

    通過(guò)觀察程序的輸出結(jié)果,以及對(duì)比 demo2 和 demo3 初始化操作不難得知,demo 對(duì)象作為左值,直接用于初始化 demo2 對(duì)象,其底層調(diào)用的是拷貝構(gòu)造函數(shù);而通過(guò)調(diào)用 move() 函數(shù)可以得到 demo 對(duì)象的右值形式,用其初始化 demo3 對(duì)象,編譯器會(huì)優(yōu)先調(diào)用移動(dòng)構(gòu)造函數(shù)。

    注意,調(diào)用拷貝構(gòu)造函數(shù),并不影響 demo 對(duì)象,但如果調(diào)用移動(dòng)構(gòu)造函數(shù),由于函數(shù)內(nèi)部會(huì)重置 demo.num 指針的指向?yàn)?NULL,所以程序中第 30 行代碼會(huì)導(dǎo)致程序運(yùn)行時(shí)發(fā)生錯(cuò)誤。

    【例 2】靈活使用 move() 函數(shù)。

    #include <iostream> using namespace std; class first { public:first() :num(new int(0)) {cout << "construct!" << endl;}//移動(dòng)構(gòu)造函數(shù)first(first &&d) :num(d.num) {d.num = NULL;cout << "first move construct!" << endl;} public: //這里應(yīng)該是 private,使用 public 是為了更方便說(shuō)明問題int *num; }; class second { public:second() :fir() {}//用 first 類的移動(dòng)構(gòu)造函數(shù)初始化 firsecond(second && sec) :fir(move(sec.fir)) {cout << "second move construct" << endl;} public: //這里也應(yīng)該是 private,使用 public 是為了更方便說(shuō)明問題first fir; }; int main() {second oth;second oth2 = move(oth);//cout << *oth.fir.num << endl; //程序報(bào)運(yùn)行時(shí)錯(cuò)誤return 0; }

    程序執(zhí)行結(jié)果為:

    construct! first move construct! second move construct

    程序中分別構(gòu)建了 first 和 second 這 2 個(gè)類,其中 second 類中包含一個(gè) first 類對(duì)象。如果讀者仔細(xì)觀察不難發(fā)現(xiàn),程序中使用了 2 此 move() 函數(shù):

    • 程序第 31 行:由于 oth 為左值,如果想調(diào)用移動(dòng)構(gòu)造函數(shù)為 oth2 初始化,需先利用 move() 函數(shù)生成一個(gè) oth 的右值版本;
    • 程序第 22 行:oth 對(duì)象內(nèi)部還包含一個(gè) first 類對(duì)象,對(duì)于 oth.fir 來(lái)說(shuō),其也是一個(gè)左值,所以在初始化 oth.fir 時(shí),還需要再調(diào)用一次 move() 函數(shù)。

    C++11完美轉(zhuǎn)發(fā)及實(shí)現(xiàn)方法詳解

    C++11 標(biāo)準(zhǔn)為 C++ 引入右值引用語(yǔ)法的同時(shí),還解決了一個(gè) C++ 98/03 標(biāo)準(zhǔn)長(zhǎng)期存在的短板,即使用簡(jiǎn)單的方式即可在函數(shù)模板中實(shí)現(xiàn)參數(shù)的完美轉(zhuǎn)發(fā)。那么,什么是完美轉(zhuǎn)發(fā)?它為什么是 C++98/03 標(biāo)準(zhǔn)存在的一個(gè)短板?C++11 標(biāo)準(zhǔn)又是如何為 C++ 彌補(bǔ)這一短板的?別急,本節(jié)將就這些問題給讀者做一一講解。

    首先解釋一下什么是完美轉(zhuǎn)發(fā),它指的是函數(shù)模板可以將自己的參數(shù)“完美”地轉(zhuǎn)發(fā)給內(nèi)部調(diào)用的其它函數(shù)。所謂完美,即不僅能準(zhǔn)確地轉(zhuǎn)發(fā)參數(shù)的值,還能保證被轉(zhuǎn)發(fā)參數(shù)的左、右值屬性不變。

    舉個(gè)例子:

    template<typename T> void function(T t) {otherdef(t); }

    如上所示,function() 函數(shù)模板中調(diào)用了 otherdef() 函數(shù)。在此基礎(chǔ)上,完美轉(zhuǎn)發(fā)指的是:如果 function() 函數(shù)接收到的參數(shù) t 為左值,那么該函數(shù)傳遞給 otherdef() 的參數(shù) t 也是左值;反之如果 function() 函數(shù)接收到的參數(shù) t 為右值,那么傳遞給 otherdef() 函數(shù)的參數(shù) t 也必須為右值。

    顯然,function() 函數(shù)模板并沒有實(shí)現(xiàn)完美轉(zhuǎn)發(fā)。一方面,參數(shù) t 為非引用類型,這意味著在調(diào)用 function() 函數(shù)時(shí),實(shí)參將值傳遞給形參的過(guò)程就需要額外進(jìn)行一次拷貝操作;另一方面,無(wú)論調(diào)用 function() 函數(shù)模板時(shí)傳遞給參數(shù) t 的是左值還是右值,對(duì)于函數(shù)內(nèi)部的參數(shù) t 來(lái)說(shuō),它有自己的名稱,也可以獲取它的存儲(chǔ)地址,因此它永遠(yuǎn)都是左值,也就是說(shuō),傳遞給 otherdef() 函數(shù)的參數(shù) t 永遠(yuǎn)都是左值。總之,無(wú)論從那個(gè)角度看,function() 函數(shù)的定義都不“完美”。

    讀者可能會(huì)問,完美轉(zhuǎn)發(fā)這樣嚴(yán)苛的參數(shù)傳遞機(jī)制,很常用嗎?C++98/03 標(biāo)準(zhǔn)中幾乎不會(huì)用到,但 C++11 標(biāo)準(zhǔn)為 C++ 引入了右值引用和移動(dòng)語(yǔ)義,因此很多場(chǎng)景中是否實(shí)現(xiàn)完美轉(zhuǎn)發(fā),直接決定了該參數(shù)的傳遞過(guò)程使用的是拷貝語(yǔ)義(調(diào)用拷貝構(gòu)造函數(shù))還是移動(dòng)語(yǔ)義(調(diào)用移動(dòng)構(gòu)造函數(shù))。

    事實(shí)上,C++98/03 標(biāo)準(zhǔn)下的 C++ 也可以實(shí)現(xiàn)完美轉(zhuǎn)發(fā),只是實(shí)現(xiàn)方式比較笨拙。通過(guò)前面的學(xué)習(xí)我們知道,C++ 98/03 標(biāo)準(zhǔn)中只有左值引用,并且可以細(xì)分為非 const 引用和 const 引用。其中,使用非 const 引用作為函數(shù)模板參數(shù)時(shí),只能接收左值,無(wú)法接收右值;而 const 左值引用既可以接收左值,也可以接收右值,但考慮到其 const 屬性,除非被調(diào)用函數(shù)的參數(shù)也是 const 屬性,否則將無(wú)法直接傳遞。

    這也就意味著,單獨(dú)使用任何一種引用形式,可以實(shí)現(xiàn)轉(zhuǎn)發(fā),但無(wú)法保證完美。因此如果使用 C++ 98/03 標(biāo)準(zhǔn)下的 C++ 語(yǔ)言,我們可以采用函數(shù)模板重載的方式實(shí)現(xiàn)完美轉(zhuǎn)發(fā),例如:

    #include <iostream> using namespace std; //重載被調(diào)用函數(shù),查看完美轉(zhuǎn)發(fā)的效果 void otherdef(int & t) {cout << "lvalue\n"; } void otherdef(const int & t) {cout << "rvalue\n"; } //重載函數(shù)模板,分別接收左值和右值 //接收右值參數(shù) template <typename T> void function(const T& t) {otherdef(t); } //接收左值參數(shù) template <typename T> void function(T& t) {otherdef(t); } int main() {function(5);//5 是右值int x = 1;function(x);//x 是左值return 0; }

    程序執(zhí)行結(jié)果為:

    rvalue lvalue

    從輸出結(jié)果中可以看到,對(duì)于右值 5 來(lái)說(shuō),它實(shí)際調(diào)用的參數(shù)類型為 const T& 的函數(shù)模板,由于 t 為 const 類型,所以 otherdef() 函數(shù)實(shí)際調(diào)用的也是參數(shù)用 const 修飾的函數(shù),所以輸出“rvalue”;對(duì)于左值 x 來(lái)說(shuō),2 個(gè)重載模板函數(shù)都適用,C++編譯器會(huì)選擇最適合的參數(shù)類型為 T& 的函數(shù)模板,進(jìn)而 therdef() 函數(shù)實(shí)際調(diào)用的是參數(shù)類型為非 const 的函數(shù),輸出“l(fā)value”。

    顯然,使用重載的模板函數(shù)實(shí)現(xiàn)完美轉(zhuǎn)發(fā)也是有弊端的,此實(shí)現(xiàn)方式僅適用于模板函數(shù)僅有少量參數(shù)的情況,否則就需要編寫大量的重載函數(shù)模板,造成代碼的冗余。為了方便用戶更快速地實(shí)現(xiàn)完美轉(zhuǎn)發(fā),C++ 11 標(biāo)準(zhǔn)中允許在函數(shù)模板中使用右值引用來(lái)實(shí)現(xiàn)完美轉(zhuǎn)發(fā)。

    C++11 標(biāo)準(zhǔn)中規(guī)定,通常情況下右值引用形式的參數(shù)只能接收右值,不能接收左值。但對(duì)于函數(shù)模板中使用右值引用語(yǔ)法定義的參數(shù)來(lái)說(shuō),它不再遵守這一規(guī)定,既可以接收右值,也可以接收左值(此時(shí)的右值引用又被稱為“萬(wàn)能引用”)。

    If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.

    如果一個(gè)變量或者參數(shù)被聲明為T&&,其中T是被推導(dǎo)的類型,那這個(gè)變量或者參數(shù)就是一個(gè)universal reference。

    仍以 function() 函數(shù)為例,在 C++11 標(biāo)準(zhǔn)中實(shí)現(xiàn)完美轉(zhuǎn)發(fā),只需要編寫如下一個(gè)模板函數(shù)即可:

    template <typename T> void function(T&& t) {otherdef(t); }

    此模板函數(shù)的參數(shù) t 既可以接收左值,也可以接收右值。但僅僅使用右值引用作為函數(shù)模板的參數(shù)是遠(yuǎn)遠(yuǎn)不夠的,還有一個(gè)問題繼續(xù)解決,即如果調(diào)用 function() 函數(shù)時(shí)為其傳遞一個(gè)左值引用或者右值引用的實(shí)參,如下所示:

    int n = 10; int & num = n; function(num); // T 為 int& int && num2 = 11; function(num2); // T 為 int &&

    其中,由 function(num) 實(shí)例化的函數(shù)底層就變成了 function(int & & t),同樣由 function(num2) 實(shí)例化的函數(shù)底層則變成了 function(int && && t)。要知道,C++98/03 標(biāo)準(zhǔn)是不支持這種用法的,而 C++ 11標(biāo)準(zhǔn)為了更好地實(shí)現(xiàn)完美轉(zhuǎn)發(fā),特意為其指定了新的類型匹配規(guī)則,又稱為引用折疊規(guī)則(假設(shè)用 A 表示實(shí)際傳遞參數(shù)的類型):

    • 當(dāng)實(shí)參為左值或者左值引用(A&)時(shí),函數(shù)模板中 T&& 將轉(zhuǎn)變?yōu)?A&(A& && = A&);
    • 當(dāng)實(shí)參為右值或者右值引用(A&&)時(shí),函數(shù)模板中 T&& 將轉(zhuǎn)變?yōu)?A&&(A&& && = A&&)。

    讀者只需要知道,在實(shí)現(xiàn)完美轉(zhuǎn)發(fā)時(shí),只要函數(shù)模板的參數(shù)類型為 T&&,則 C++ 可以自行準(zhǔn)確地判定出實(shí)際傳入的實(shí)參是左值還是右值。

    通過(guò)將函數(shù)模板的形參類型設(shè)置為 T&&,我們可以很好地解決接收左、右值的問題。但除此之外,還需要解決一個(gè)問題,即無(wú)論傳入的形參是左值還是右值,對(duì)于函數(shù)模板內(nèi)部來(lái)說(shuō),形參既有名稱又能尋址,因此它都是左值。那么如何才能將函數(shù)模板接收到的形參連同其左、右值屬性,一起傳遞給被調(diào)用的函數(shù)呢?

    C++11 標(biāo)準(zhǔn)的開發(fā)者已經(jīng)幫我們想好的解決方案,該新標(biāo)準(zhǔn)還引入了一個(gè)模板函數(shù) forward<T>(),我們只需要調(diào)用該函數(shù),就可以很方便地解決此問題。仍以 function 模板函數(shù)為例,如下演示了該函數(shù)模板的用法:

    #include <iostream> using namespace std; //重載被調(diào)用函數(shù),查看完美轉(zhuǎn)發(fā)的效果 void otherdef(int & t) {cout << "lvalue\n"; } void otherdef(const int & t) {cout << "rvalue\n"; } //實(shí)現(xiàn)完美轉(zhuǎn)發(fā)的函數(shù)模板 template <typename T> void function(T&& t) {otherdef(forward<T>(t)); } int main() {function(5);int x = 1;function(x);return 0; }

    程序執(zhí)行結(jié)果為:

    rvalue lvalue

    注意程序中第 12~16 行,此 function() 模板函數(shù)才是實(shí)現(xiàn)完美轉(zhuǎn)發(fā)的最終版本。可以看到,forward() 函數(shù)模板用于修飾被調(diào)用函數(shù)中需要維持參數(shù)左、右值屬性的參數(shù)。

    總的來(lái)說(shuō),在定義模板函數(shù)時(shí),我們采用右值引用的語(yǔ)法格式定義參數(shù)類型,由此該函數(shù)既可以接收外界傳入的左值,也可以接收右值;其次,還需要使用 C++11 標(biāo)準(zhǔn)庫(kù)提供的 forward() 模板函數(shù)修飾被調(diào)用函數(shù)中需要維持左、右值屬性的參數(shù)。由此即可輕松實(shí)現(xiàn)函數(shù)模板中參數(shù)的完美轉(zhuǎn)發(fā)。

    Ref:

    http://c.biancheng.net/view/7829.html

    http://c.biancheng.net/view/7847.html

    http://c.biancheng.net/view/7863.html

    http://c.biancheng.net/view/7868.html

    https://cloud.tencent.com/developer/article/1561681

    總結(jié)

    以上是生活随笔為你收集整理的C++11 右值引用、移动语义、完美转发、万能引用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。