C++11新特性--右值引用與移動(dòng)語義

引子-深拷貝和淺拷貝

????在cpp11之前,我們定義一個(gè)類如果類中有指針成員,并且其指向一塊堆內(nèi)存,那么往往本類要負(fù)責(zé)這個(gè)指針指向內(nèi)存的分配和銷毀,不然會(huì)產(chǎn)生令人討厭的內(nèi)存泄露問題。但如果這個(gè)類的對(duì)象之間進(jìn)行復(fù)制就會(huì)涉及到數(shù)據(jù)的拷貝問題,如果不加處理,使用編譯器默認(rèn)生成的拷貝構(gòu)造函數(shù),那么默認(rèn)的行為就是按bit進(jìn)行memory copy,這就是所謂的淺拷貝。淺拷貝的后果是兩個(gè)對(duì)象持有兩個(gè)不同的成員指針,兩個(gè)指針指向同樣的堆內(nèi)存,如果其中一個(gè)銷毀了內(nèi)存,另一個(gè)再用,就會(huì)產(chǎn)生無法預(yù)知的錯(cuò)誤,導(dǎo)致程序錯(cuò)誤。所以針對(duì)這種情況我們往往要提供自定義版本的拷貝構(gòu)造函數(shù),來確保指針指向的內(nèi)存也有一份拷貝。可以看一下代碼示例:

class PtrMemTest
{
public:
    int* p;
    PtrMemTest(int value)
    {
        p = new int(value);
    }
    ~PtrMemTest()
    {
        if (p)
        {
            delete p;
            p = nullptr;
        }
    }
    PtrMemTest(const PtrMemTest& obj)
    {
        p = new int(*obj.p);
    }
};

????如果我們不定義拷貝構(gòu)造函數(shù),那么就會(huì)引起淺拷貝的問題,繼而可能引起嚴(yán)重的程序錯(cuò)誤。提供了拷貝構(gòu)造函數(shù)之后,我們保證了程序的正確性,但是內(nèi)存的頻繁拷貝是一種性能開銷,如果允許我們?cè)谝恍┣闆r下只是把指針的指向的內(nèi)存所有權(quán)轉(zhuǎn)移應(yīng)該如何呢?比如下面的代碼

PtrMemTest obj2(PtrMemTest(12));

????這里只是用一個(gè)臨時(shí)對(duì)象去初始化另一個(gè)對(duì)象,會(huì)產(chǎn)生兩次堆內(nèi)存的申請(qǐng),其中一次實(shí)際完全沒有必要,因?yàn)槭莻€(gè)臨時(shí)對(duì)象,完成了第二個(gè)對(duì)象的初始化之后就馬上銷毀了。這個(gè)例子比較極端,在平時(shí)程序中不常見,那么用一個(gè)函數(shù)返回值去初始化一個(gè)對(duì)象的場(chǎng)景就極為常見了。如代碼

PtrMemTest GetMemObj(int value)
{
    return PtrMemTest(value);
}
void testMem()
{
    PtrMemTest obj3(GetMemObj(13));
}

????這種情況下跟上面情況類似,函數(shù)的返回值作為一個(gè)臨時(shí)對(duì)象,去初始化obj3這個(gè)對(duì)象,完成使命后就自行銷毀,產(chǎn)生了不必要或者說可以優(yōu)化的內(nèi)存拷貝。這種情況實(shí)際上會(huì)產(chǎn)生一次構(gòu)造和兩次拷貝構(gòu)造(實(shí)際編譯器會(huì)對(duì)這種情況做優(yōu)化,從而不會(huì)產(chǎn)生額外的內(nèi)存釋放和銷毀),一次是PtrMemTest(value)產(chǎn)生的構(gòu)造,一次是函數(shù)GetMemObj中產(chǎn)生的一個(gè)臨時(shí)對(duì)象作為函數(shù)的返回值,而它用剛才的對(duì)象進(jìn)行構(gòu)造,從而產(chǎn)生了一次拷貝構(gòu)造,最后一次拷貝構(gòu)造發(fā)生在構(gòu)造obj3的時(shí)候。那么如果可以有效利用臨時(shí)對(duì)象,把它們的內(nèi)存“偷過來“就可以減少一次有可能成本十分昂貴的拷貝構(gòu)造。在cpp11中,有了對(duì)這些場(chǎng)景的一個(gè)解決方案--右值引用。

什么是右值

????在c中我們可以近似的認(rèn)為賦值號(hào)左邊的稱為左值(lvalue)右邊的稱為右值(rvalue)。如

int a = 3;
int b = a + 5;

????其中a和b就是左值,3和a+5都是右值。
????還有一個(gè)比較被廣泛認(rèn)同的定義,可以被合法取地址的值稱為左值,反之稱為右值。如&a, &b都是合法表達(dá)式,所以他們都是左值;但是&3,&(a+5)都是不合法的表達(dá)式,所以他們不能取地址,進(jìn)而它們即是右值。那么容易看出來函數(shù)的返回值是一個(gè)臨時(shí)值,無法被取地址,所以是個(gè)右值。

右值引用

????cpp11中右值引用就是對(duì)一個(gè)右值進(jìn)行引用的類型。由于右值通常不具有名字,我們也只能通過引用的方式綁定它。如

int&& a = ReturnIntRvalue();
int&& b = 12;

int& c = ReturnIntRvalue();//compile fail
const int& d = ReturnIntRvalue();

????上面代碼中前兩句都是右值引用綁定右值的例子,右值引用只能綁定到右值上,否則會(huì)編譯失敗。值得一提的是,第三句無法編譯通過,由于cpp不允許左值引用綁定到右值上,但是第四句卻能編譯通過,原因是const T&類型是萬能類型,可以綁定到任何類型,左值,常量左值,右值。不過這里相對(duì)于右值引用,它是只讀的,而右值引用是可以改變所引用的右值的值的。
再看一下代碼

class PtrMemTest
{
public:
    int* p;
    PtrMemTest(int value)
    {
        std::cout<<"PtrMemTest"<<std::endl;
        p = new int(value);
    }
    ~PtrMemTest()
    {
        std::cout<<"~PtrMemTest"<<std::endl;
        if (p)
        {
            delete p;
            p = nullptr;
        }
    }
    PtrMemTest(const PtrMemTest& obj)
    {
        std::cout<<"PtrMemTest copy"<<std::endl;
        p = new int(*obj.p);
    }

    PtrMemTest(PtrMemTest&& obj)
    {
        std::cout<<"PtrMemTest move"<<std::endl;
        p = obj.p;
        obj.p = nullptr;
    }
};
    static void execute()
    {
        PtrMemTest obj1(111);
        PtrMemTest obj2(std::move(obj1));
    }

????這里對(duì)前面的代碼做了增加,增加了一個(gè)cpp11中新增的移動(dòng)構(gòu)造函數(shù),它的作用正是前面我們希望的將一個(gè)右值(臨時(shí)對(duì)象)的內(nèi)容“偷”過來,用最小的代價(jià)來構(gòu)造新的對(duì)象。move這個(gè)庫函數(shù),用來強(qiáng)制將一個(gè)值轉(zhuǎn)換成右值。所以這里運(yùn)行的結(jié)果是

PtrMemTest
PtrMemTest move
~PtrMemTest
~PtrMemTest

????obj2將自己的成員指針指向了obj1中開辟的內(nèi)存,obj1此后變成了一個(gè)空對(duì)象。這里為了規(guī)避編譯器的優(yōu)化,所以寫成了這種方式,這種方式是有風(fēng)險(xiǎn)的,因?yàn)樵趏bj2構(gòu)造后,程序就不應(yīng)該再使用obj1了,否則可能出現(xiàn)問題。更合理的用法是

 PtrMemTest obj2(GetPtrMemObj());

????這樣函數(shù)產(chǎn)生的臨時(shí)返回值就會(huì)被“移動(dòng)”到obj2中,從而減少了內(nèi)存的分配和銷毀。

移動(dòng)語義

????前面的例子所展示即為移動(dòng)語義。標(biāo)準(zhǔn)庫中提供了一個(gè)有用的函數(shù)std::move來強(qiáng)制將左值轉(zhuǎn)換成右值,正如剛才例子中所展示的。有了這個(gè)標(biāo)準(zhǔn)庫函數(shù),我們可以更加靈活的按自己的需求來將左值轉(zhuǎn)換成右值。比如繼承一個(gè)有移動(dòng)構(gòu)造的基類,子類并不增加數(shù)據(jù),只是擴(kuò)展了一些函數(shù),那么如果此時(shí)需要提供自己的版本的移動(dòng)構(gòu)造函數(shù)以延續(xù)父類的移動(dòng)語義,這時(shí)你可能就明確需要move函數(shù)了。如下代碼

class PtrMemTest
{
public:
    int* p;
    PtrMemTest(int value)
    {
        std::cout<<"PtrMemTest"<<std::endl;
        p = new int(value);
    }
    ~PtrMemTest()
    {
        std::cout<<"~PtrMemTest"<<std::endl;
        if (p)
        {
            delete p;
            p = nullptr;
        }
    }
    PtrMemTest(const PtrMemTest& obj)
    {
        std::cout<<"PtrMemTest copy"<<std::endl;
        p = new int(*obj.p);
    }

    PtrMemTest(PtrMemTest&& obj)
    {
        std::cout<<"PtrMemTest move"<<std::endl;
        p = obj.p;
        obj.p = nullptr;
    }
};

class PtrMemTestDerive : public PtrMemTest
{
public:
    PtrMemTestDerive(int value) : PtrMemTest(value)
    {
        std::cout<<"PtrMemTestDerive"<<std::endl;
    }

    PtrMemTestDerive(const PtrMemTestDerive &obj) : PtrMemTest(obj)
    {
        std::cout<<"PtrMemTestDerive copy"<<std::endl;
    }

    PtrMemTestDerive(PtrMemTestDerive &&obj) : PtrMemTest(obj)
    {
        std::cout<<"PtrMemTestDerive move"<<std::endl;
    }

    //........

};
void test()
{
    PtrMemTestDerive obj1(111);
    PtrMemTestDerive obj3(std::move(obj1));
}

????這段代碼并沒有按照你所期望的行使移動(dòng)語義,因?yàn)镻trMemTestDerive(PtrMemTestDerive &&obj) : PtrMemTest(obj)雖然子類中傳入了右值引用,但是將obj像父類的移動(dòng)構(gòu)造(其實(shí)是拷貝構(gòu)造)傳遞的時(shí)候,obj是個(gè)左值,所以并沒有如預(yù)期的調(diào)用父類的移動(dòng)構(gòu)造函數(shù),而是調(diào)用了父類的拷貝構(gòu)造函數(shù)。所以這里move可以顯示它的作用了。

class PtrMemTestDerive : public PtrMemTest
{
public:
    PtrMemTestDerive(int value) : PtrMemTest(value)
    {
        std::cout<<"PtrMemTestDerive"<<std::endl;
    }

    PtrMemTestDerive(const PtrMemTestDerive &obj) : PtrMemTest(obj)
    {
        std::cout<<"PtrMemTestDerive copy"<<std::endl;
    }

    PtrMemTestDerive(PtrMemTestDerive &&obj) : PtrMemTest(std::move(obj))
    {
        std::cout<<"PtrMemTestDerive move"<<std::endl;
    }

    //........

};

新的子類用了move將obj繼續(xù)作為右值傳遞給父類,順利的調(diào)用父類的移動(dòng)構(gòu)造函數(shù)。

std::move

????上面的示例用到了很多STL庫提供的新函數(shù)move函數(shù),前面提到過的它的作用是強(qiáng)制將左值轉(zhuǎn)換成右值引用。注意返回的是一個(gè)右值引用,而不是右值。但是直接使用move的返回值即得到一個(gè)右值,而不是一個(gè)右值引用。比較繞,看一下STL庫中的注釋:
Move as rvalue
Returns an rvalue reference to arg.
This is a helper function to force move semantics on values, even if they have a name: Directly using the returned value causes arg to be considered an rvalue.
大概翻譯一下就是返回一個(gè)右值引用,直接用它的返回值就被當(dāng)成一個(gè)右值用。
它的應(yīng)用場(chǎng)景前面已經(jīng)演示了,那么我們簡(jiǎn)單看一下它的實(shí)現(xiàn)。

template <class _Tp>
inline
typename remove_reference<_Tp>::type&&
move(_Tp&& __t)
{
    typedef typename remove_reference<_Tp>::type _Up;
    return static_cast<_Up&&>(__t);
}

????上面代碼去掉了一些宏定義,留下了骨干內(nèi)容,下面分析一下這個(gè)很短小的函數(shù)。首先它是一個(gè)模板函數(shù),行參_Tp&& __t這里并不代表是_Tp類型的右值引用,而是有其特定規(guī)則的,這里后續(xù)在完美轉(zhuǎn)發(fā)語義中會(huì)詳細(xì)講到,我們目前只需認(rèn)為這里經(jīng)過推倒后_Tp的類型可能是左值,左值引用,常量左值引用,和右值引用就好了。看下面的函數(shù)體通過字面意思remove_reference這個(gè)模板類的唯一作用是萃取出_Tp的原始非引用類型。也就是說_Up是個(gè)左值類型,這樣最后返回的很明確,不管傳入什么,最后傳出都是一個(gè)右值引用。
????關(guān)于move我們?cè)诤罄m(xù)的完美轉(zhuǎn)發(fā)中還會(huì)繼續(xù)分析。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,155評(píng)論 3 425
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,255評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,646評(píng)論 1 326
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,838評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,399評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,146評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,338評(píng)論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,565評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評(píng)論 1 292
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,059評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,296評(píng)論 2 376

推薦閱讀更多精彩內(nèi)容