[c++11]我理解的右值引用、移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)

c++中引入了右值引用移動(dòng)語(yǔ)義箕宙,可以避免無(wú)謂的復(fù)制嚎朽,提高程序性能。有點(diǎn)難理解柬帕,于是花時(shí)間整理一下自己的理解哟忍。

左值、右值

C++中所有的值都必然屬于左值陷寝、右值二者之一锅很。左值是指表達(dá)式結(jié)束后依然存在的持久化對(duì)象,右值是指表達(dá)式結(jié)束時(shí)就不再存在的臨時(shí)對(duì)象凤跑。所有的具名變量或者對(duì)象都是左值爆安,而右值不具名。很難得到左值和右值的真正定義仔引,但是有一個(gè)可以區(qū)分左值和右值的便捷方法:看能不能對(duì)表達(dá)式取地址扔仓,如果能,則為左值咖耘,否則為右值翘簇。

看見(jiàn)書(shū)上又將右值分為將亡值和純右值。純右值就是c++98標(biāo)準(zhǔn)中右值的概念儿倒,如非引用返回的函數(shù)返回的臨時(shí)變量值版保;一些運(yùn)算表達(dá)式,如1+2產(chǎn)生的臨時(shí)變量;不跟對(duì)象關(guān)聯(lián)的字面量值彻犁,如2蹈垢,'c',true袖裕,"hello"曹抬;這些值都不能夠被取地址。

而將亡值則是c++11新增的和右值引用相關(guān)的表達(dá)式急鳄,這樣的表達(dá)式通常時(shí)將要移動(dòng)的對(duì)象谤民、T&&函數(shù)返回值、std::move()函數(shù)的返回值等疾宏,

不懂將亡值和純右值的區(qū)別其實(shí)沒(méi)關(guān)系张足,統(tǒng)一看作右值即可,不影響使用坎藐。

示例:

int i=0;// i是左值为牍, 0是右值

class A {
  public:
    int a;
};
A getTemp()
{
    return A();
}
A a = getTemp();   // a是左值  getTemp()的返回值是右值(臨時(shí)變量)

左值引用、右值引用

c++98中的引用很常見(jiàn)了岩馍,就是給變量取了個(gè)別名碉咆,在c++11中,因?yàn)樵黾恿?strong>右值引用(rvalue reference)的概念蛀恩,所以c++98中的引用都稱(chēng)為了左值引用(lvalue reference)疫铜。

int a = 10; 
int& refA = a; // refA是a的別名, 修改refA就是修改a, a是左值双谆,左移是左值引用

int& b = 1; //編譯錯(cuò)誤! 1是右值壳咕,不能夠使用左值引用

c++11中的右值引用使用的符號(hào)是&&,如

int&& a = 1; //實(shí)質(zhì)上就是將不具名(匿名)變量取了個(gè)別名
int b = 1;
int && c = b; //編譯錯(cuò)誤顽馋! 不能將一個(gè)左值復(fù)制給一個(gè)右值引用
class A {
  public:
    int a;
};
A getTemp()
{
    return A();
}
A && a = getTemp();   //getTemp()的返回值是右值(臨時(shí)變量)

getTemp()返回的右值本來(lái)在表達(dá)式語(yǔ)句結(jié)束后谓厘,其生命也就該終結(jié)了(因?yàn)槭桥R時(shí)變量),而通過(guò)右值引用寸谜,該右值又重獲新生竟稳,其生命期將與右值引用類(lèi)型變量a的生命期一樣,只要a還活著程帕,該右值臨時(shí)變量將會(huì)一直存活下去住练。實(shí)際上就是給那個(gè)臨時(shí)變量取了個(gè)名字地啰。

注意:這里a類(lèi)型是右值引用類(lèi)型(int &&)愁拭,但是如果從左值和右值的角度區(qū)分它,它實(shí)際上是個(gè)左值亏吝。因?yàn)榭梢詫?duì)它取地址岭埠,而且它還有名字,是一個(gè)已經(jīng)命名的右值。

所以惜论,左值引用只能綁定左值许赃,右值引用只能綁定右值,如果綁定的不對(duì)馆类,編譯就會(huì)失敗混聊。但是,常量左值引用卻是個(gè)奇葩乾巧,它可以算是一個(gè)“萬(wàn)能”的引用類(lèi)型句喜,它可以綁定非常量左值、常量左值沟于、右值咳胃,而且在綁定右值的時(shí)候,常量左值引用還可以像右值引用一樣將右值的生命期延長(zhǎng)旷太,缺點(diǎn)是展懈,只能讀不能改。

const int & a = 1; //常量左值引用綁定 右值供璧, 不會(huì)報(bào)錯(cuò)

class A {
  public:
    int a;
};
A getTemp()
{
    return A();
}
const A & a = getTemp();   //不會(huì)報(bào)錯(cuò) 而 A& a 會(huì)報(bào)錯(cuò)

事實(shí)上存崖,很多情況下我們用來(lái)常量左值引用的這個(gè)功能卻沒(méi)有意識(shí)到,如下面的例子:

#include <iostream>
using namespace std;

class Copyable {
public:
    Copyable(){}
    Copyable(const Copyable &o) {
        cout << "Copied" << endl;
    }
};
Copyable ReturnRvalue() {
    return Copyable(); //返回一個(gè)臨時(shí)對(duì)象
}
void AcceptVal(Copyable a) {

}
void AcceptRef(const Copyable& a) {

}

int main() {
    cout << "pass by value: " << endl;
    AcceptVal(ReturnRvalue()); // 應(yīng)該調(diào)用兩次拷貝構(gòu)造函數(shù)
    cout << "pass by reference: " << endl;
    AcceptRef(ReturnRvalue()); //應(yīng)該只調(diào)用一次拷貝構(gòu)造函數(shù)
}

當(dāng)我敲完上面的例子并運(yùn)行后睡毒,發(fā)現(xiàn)結(jié)果和我想象的完全不一樣金句!期望AcceptVal(ReturnRvalue())需要調(diào)用兩次拷貝構(gòu)造函數(shù),一次在ReturnRvalue()函數(shù)中吕嘀,構(gòu)造好了Copyable對(duì)象违寞,返回的時(shí)候會(huì)調(diào)用拷貝構(gòu)造函數(shù)生成一個(gè)臨時(shí)對(duì)象,在調(diào)用AcceptVal()時(shí)偶房,又會(huì)將這個(gè)對(duì)象拷貝給函數(shù)的局部變量a趁曼,一共調(diào)用了兩次拷貝構(gòu)造函數(shù)。而AcceptRef()的不同在于形參是常量左值引用棕洋,它能夠接收一個(gè)右值挡闰,而且不需要拷貝。

而實(shí)際的結(jié)果是掰盘,不管哪種方式摄悯,一次拷貝構(gòu)造函數(shù)都沒(méi)有調(diào)用!

這是由于編譯器默認(rèn)開(kāi)啟了返回值優(yōu)化(RVO/NRVO, RVO, Return Value Optimization 返回值優(yōu)化愧捕,或者NRVO奢驯, Named Return Value Optimization)。編譯器很聰明次绘,發(fā)現(xiàn)在ReturnRvalue內(nèi)部生成了一個(gè)對(duì)象瘪阁,返回之后還需要生成一個(gè)臨時(shí)對(duì)象調(diào)用拷貝構(gòu)造函數(shù)撒遣,很麻煩,所以直接優(yōu)化成了1個(gè)對(duì)象對(duì)象管跺,避免拷貝义黎,而這個(gè)臨時(shí)變量又被賦值給了函數(shù)的形參,還是沒(méi)必要豁跑,所以最后這三個(gè)變量都用一個(gè)變量替代了廉涕,不需要調(diào)用拷貝構(gòu)造函數(shù)。

雖然各大廠家的編譯器都已經(jīng)都有了這個(gè)優(yōu)化艇拍,但是這并不是c++標(biāo)準(zhǔn)規(guī)定的火的,而且不是所有的返回值都能夠被優(yōu)化,而這篇文章的主要講的右值引用淑倾,移動(dòng)語(yǔ)義可以解決編譯器無(wú)法解決的問(wèn)題馏鹤。

為了更好的觀察結(jié)果,可以在編譯的時(shí)候加上-fno-elide-constructors選項(xiàng)(關(guān)閉返回值優(yōu)化)娇哆。

// g++ test.cpp -o test -fno-elide-constructors
pass by value: 
Copied
Copied //可以看到確實(shí)調(diào)用了兩次拷貝構(gòu)造函數(shù)
pass by reference: 
Copied

上面這個(gè)例子本意是想說(shuō)明常量左值引用能夠綁定一個(gè)右值湃累,可以減少一次拷貝(使用非常量的左值引用會(huì)編譯失敗)碍讨,但是順便講到了編譯器的返回值優(yōu)化治力。。編譯器還是干了很多事情的勃黍,很有用宵统,但不能過(guò)于依賴(lài),因?yàn)槟阋膊淮_定它什么時(shí)候優(yōu)化了什么時(shí)候沒(méi)優(yōu)化覆获。

總結(jié)一下马澈,其中T是一個(gè)具體類(lèi)型:

  1. 左值引用, 使用 T&, 只能綁定左值
  2. 右值引用弄息, 使用 T&&痊班, 只能綁定右值
  3. 常量左值, 使用 const T&, 既可以綁定左值又可以綁定右值
  4. 已命名的右值引用摹量,編譯器會(huì)認(rèn)為是個(gè)左值
  5. 編譯器有返回值優(yōu)化涤伐,但不要過(guò)于依賴(lài)

移動(dòng)構(gòu)造和移動(dòng)賦值

回顧一下如何用c++實(shí)現(xiàn)一個(gè)字符串類(lèi)MyStringMyString內(nèi)部管理一個(gè)C語(yǔ)言的char *數(shù)組缨称,這個(gè)時(shí)候一般都需要實(shí)現(xiàn)拷貝構(gòu)造函數(shù)和拷貝賦值函數(shù)凝果,因?yàn)槟J(rèn)的拷貝是淺拷貝,而指針這種資源不能共享睦尽,不然一個(gè)析構(gòu)了器净,另一個(gè)也就完蛋了。

具體代碼如下:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class MyString
{
public:
    static size_t CCtor; //統(tǒng)計(jì)調(diào)用拷貝構(gòu)造函數(shù)的次數(shù)
//    static size_t CCtor; //統(tǒng)計(jì)調(diào)用拷貝構(gòu)造函數(shù)的次數(shù)
public:
    // 構(gòu)造函數(shù)
   MyString(const char* cstr=0){
       if (cstr) {
          m_data = new char[strlen(cstr)+1];
          strcpy(m_data, cstr);
       }
       else {
          m_data = new char[1];
          *m_data = '\0';
       }
   }

   // 拷貝構(gòu)造函數(shù)
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }
   // 拷貝賦值函數(shù) =號(hào)重載
   MyString& operator=(const MyString& str){
       if (this == &str) // 避免自我賦值!!
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   ~MyString() {
       delete[] m_data;
   }

   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};
size_t MyString::CCtor = 0;

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000個(gè)空間骂删,不這么做掌动,調(diào)用的次數(shù)可能遠(yuǎn)大于1000
    for(int i=0;i<1000;i++){
        vecStr.push_back(MyString("hello"));
    }
    cout << MyString::CCtor << endl;
}

代碼看起來(lái)挺不錯(cuò),卻發(fā)現(xiàn)執(zhí)行了1000次拷貝構(gòu)造函數(shù)宁玫,如果MyString("hello")構(gòu)造出來(lái)的字符串本來(lái)就很長(zhǎng)粗恢,構(gòu)造一遍就很耗時(shí)了,最后卻還要拷貝一遍欧瘪,而MyString("hello")只是臨時(shí)對(duì)象眷射,拷貝完就沒(méi)什么用了,這就造成了沒(méi)有意義的資源申請(qǐng)和釋放操作佛掖,如果能夠直接使用臨時(shí)對(duì)象已經(jīng)申請(qǐng)的資源妖碉,既能節(jié)省資源,又能節(jié)省資源申請(qǐng)和釋放的時(shí)間芥被。而C++11新增加的移動(dòng)語(yǔ)義就能夠做到這一點(diǎn)欧宜。

要實(shí)現(xiàn)移動(dòng)語(yǔ)義就必須增加兩個(gè)函數(shù):移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值構(gòu)造函數(shù)。

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class MyString
{
public:
    static size_t CCtor; //統(tǒng)計(jì)調(diào)用拷貝構(gòu)造函數(shù)的次數(shù)
    static size_t MCtor; //統(tǒng)計(jì)調(diào)用移動(dòng)構(gòu)造函數(shù)的次數(shù)
    static size_t CAsgn; //統(tǒng)計(jì)調(diào)用拷貝賦值函數(shù)的次數(shù)
    static size_t MAsgn; //統(tǒng)計(jì)調(diào)用移動(dòng)賦值函數(shù)的次數(shù)

public:
    // 構(gòu)造函數(shù)
   MyString(const char* cstr=0){
       if (cstr) {
          m_data = new char[strlen(cstr)+1];
          strcpy(m_data, cstr);
       }
       else {
          m_data = new char[1];
          *m_data = '\0';
       }
   }

   // 拷貝構(gòu)造函數(shù)
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }
   // 移動(dòng)構(gòu)造函數(shù)
   MyString(MyString&& str) noexcept
       :m_data(str.m_data) {
       MCtor ++;
       str.m_data = nullptr; //不再指向之前的資源了
   }

   // 拷貝賦值函數(shù) =號(hào)重載
   MyString& operator=(const MyString& str){
       CAsgn ++;
       if (this == &str) // 避免自我賦值!!
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   // 移動(dòng)賦值函數(shù) =號(hào)重載
   MyString& operator=(MyString&& str) noexcept{
       MAsgn ++;
       if (this == &str) // 避免自我賦值!!
          return *this;

       delete[] m_data;
       m_data = str.m_data;
       str.m_data = nullptr; //不再指向之前的資源了
       return *this;
   }

   ~MyString() {
       delete[] m_data;
   }

   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};
size_t MyString::CCtor = 0;
size_t MyString::MCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MAsgn = 0;
int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000個(gè)空間
    for(int i=0;i<1000;i++){
        vecStr.push_back(MyString("hello"));
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;
}

/* 結(jié)果
CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/

可以看到拴魄,移動(dòng)構(gòu)造函數(shù)與拷貝構(gòu)造函數(shù)的區(qū)別是冗茸,拷貝構(gòu)造的參數(shù)是const MyString& str,是常量左值引用匹中,而移動(dòng)構(gòu)造的參數(shù)是MyString&& str夏漱,是右值引用颊咬,而MyString("hello")是個(gè)臨時(shí)對(duì)象沃粗,是個(gè)右值,優(yōu)先進(jìn)入移動(dòng)構(gòu)造函數(shù)而不是拷貝構(gòu)造函數(shù)项乒。而移動(dòng)構(gòu)造函數(shù)與拷貝構(gòu)造不同服赎,它并不是重新分配一塊新的空間葵蒂,將要拷貝的對(duì)象復(fù)制過(guò)來(lái),而是"偷"了過(guò)來(lái)重虑,將自己的指針指向別人的資源刹勃,然后將別人的指針修改為nullptr,這一步很重要嚎尤,如果不將別人的指針修改為空荔仁,那么臨時(shí)對(duì)象析構(gòu)的時(shí)候就會(huì)釋放掉這個(gè)資源,"偷"也白偷了芽死。下面這張圖可以解釋copy和move的區(qū)別乏梁。

copy和move的區(qū)別.png

不用奇怪為什么可以搶別人的資源,臨時(shí)對(duì)象的資源不好好利用也是浪費(fèi)关贵,因?yàn)樯芷诒緛?lái)就是很短遇骑,在你執(zhí)行完這個(gè)表達(dá)式之后,它就毀滅了揖曾,充分利用資源落萎,才能很高效亥啦。

對(duì)于一個(gè)左值,肯定是調(diào)用拷貝構(gòu)造函數(shù)了练链,但是有些左值是局部變量翔脱,生命周期也很短,能不能也移動(dòng)而不是拷貝呢媒鼓?C++11為了解決這個(gè)問(wèn)題届吁,提供了std::move()方法來(lái)將左值轉(zhuǎn)換為右值,從而方便應(yīng)用移動(dòng)語(yǔ)義绿鸣。我覺(jué)得它其實(shí)就是告訴編譯器疚沐,雖然我是一個(gè)左值,但是不要對(duì)我用拷貝構(gòu)造函數(shù)潮模,而是用移動(dòng)構(gòu)造函數(shù)吧亮蛔。。擎厢。

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000個(gè)空間
    for(int i=0;i<1000;i++){
        MyString tmp("hello");
        vecStr.push_back(tmp); //調(diào)用的是拷貝構(gòu)造函數(shù)
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;

    cout << endl;
    MyString::CCtor = 0;
    MyString::MCtor = 0;
    MyString::CAsgn = 0;
    MyString::MAsgn = 0;
    vector<MyString> vecStr2;
    vecStr2.reserve(1000); //先分配好1000個(gè)空間
    for(int i=0;i<1000;i++){
        MyString tmp("hello");
        vecStr2.push_back(std::move(tmp)); //調(diào)用的是移動(dòng)構(gòu)造函數(shù)
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;
}

/* 運(yùn)行結(jié)果
CCtor = 1000
MCtor = 0
CAsgn = 0
MAsgn = 0

CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/

下面再舉幾個(gè)例子:

MyString str1("hello"); //調(diào)用構(gòu)造函數(shù)
MyString str2("world"); //調(diào)用構(gòu)造函數(shù)
MyString str3(str1); //調(diào)用拷貝構(gòu)造函數(shù)
MyString str4(std::move(str1)); // 調(diào)用移動(dòng)構(gòu)造函數(shù)尔邓、
//    cout << str1.get_c_str() << endl; // 此時(shí)str1的內(nèi)部指針已經(jīng)失效了!不要使用
//注意:雖然str1中的m_dat已經(jīng)稱(chēng)為了空锉矢,但是str1這個(gè)對(duì)象還活著梯嗽,知道出了它的作用域才會(huì)析構(gòu)!而不是move完了立刻析構(gòu)
MyString str5;
str5 = str2; //調(diào)用拷貝賦值函數(shù)
MyString str6;
str6 = std::move(str2); // str2的內(nèi)容也失效了沽损,不要再使用

需要注意一下幾點(diǎn):

  1. str6 = std::move(str2)灯节,雖然將str2的資源給了str6,但是str2并沒(méi)有立刻析構(gòu)绵估,只有在str2離開(kāi)了自己的作用域的時(shí)候才會(huì)析構(gòu)炎疆,所以,如果繼續(xù)使用str2m_data變量国裳,可能會(huì)發(fā)生意想不到的錯(cuò)誤形入。
  2. 如果我們沒(méi)有提供移動(dòng)構(gòu)造函數(shù),只提供了拷貝構(gòu)造函數(shù)缝左,std::move()會(huì)失效但是不會(huì)發(fā)生錯(cuò)誤亿遂,因?yàn)榫幾g器找不到移動(dòng)構(gòu)造函數(shù)就去尋找拷貝構(gòu)造函數(shù),也這是拷貝構(gòu)造函數(shù)的參數(shù)是const T&常量左值引用的原因渺杉!
  3. c++11中的所有容器都實(shí)現(xiàn)了move語(yǔ)義蛇数,move只是轉(zhuǎn)移了資源的控制權(quán),本質(zhì)上是將左值強(qiáng)制轉(zhuǎn)化為右值使用是越,以用于移動(dòng)拷貝或賦值耳舅,避免對(duì)含有資源的對(duì)象發(fā)生無(wú)謂的拷貝。move對(duì)于擁有如內(nèi)存倚评、文件句柄等資源的成員的對(duì)象有效浦徊,如果是一些基本類(lèi)型馏予,如int和char[10]數(shù)組等,如果使用move盔性,仍會(huì)發(fā)生拷貝(因?yàn)闆](méi)有對(duì)應(yīng)的移動(dòng)構(gòu)造函數(shù))霞丧,所以說(shuō)move對(duì)含有資源的對(duì)象說(shuō)更有意義。

universal references(通用引用)

當(dāng)右值引用和模板結(jié)合的時(shí)候纯出,就復(fù)雜了蚯妇。T&&并不一定表示右值引用敷燎,它可能是個(gè)左值引用又可能是個(gè)右值引用暂筝。例如:

template<typename T>
void f( T&& param){
    
}
f(10);  //10是右值
int x = 10; //
f(x); //x是左值

如果上面的函數(shù)模板表示的是右值引用的話,肯定是不能傳遞左值的硬贯,但是事實(shí)卻是可以焕襟。這里的&&是一個(gè)未定義的引用類(lèi)型,稱(chēng)為universal references饭豹,它必須被初始化鸵赖,它是左值引用還是右值引用卻決于它的初始化,如果它被一個(gè)左值初始化拄衰,它就是一個(gè)左值引用它褪;如果被一個(gè)右值初始化,它就是一個(gè)右值引用翘悉。

注意:只有當(dāng)發(fā)生自動(dòng)類(lèi)型推斷時(shí)(如函數(shù)模板的類(lèi)型自動(dòng)推導(dǎo)茫打,或auto關(guān)鍵字),&&才是一個(gè)universal references妖混。

例如:

template<typename T>
void f( T&& param); //這里T的類(lèi)型需要推導(dǎo)老赤,所以&&是一個(gè) universal references

template<typename T>
class Test {
  Test(Test&& rhs); //Test是一個(gè)特定的類(lèi)型,不需要類(lèi)型推導(dǎo)制市,所以&&表示右值引用  
};

void f(Test&& param); //右值引用

//復(fù)雜一點(diǎn)
template<typename T>
void f(std::vector<T>&& param); //在調(diào)用這個(gè)函數(shù)之前抬旺,這個(gè)vector<T>中的推斷類(lèi)型
//已經(jīng)確定了,所以調(diào)用f函數(shù)的時(shí)候沒(méi)有類(lèi)型推斷了祥楣,所以是 右值引用

template<typename T>
void f(const T&& param); //右值引用
// universal references僅僅發(fā)生在 T&& 下面开财,任何一點(diǎn)附加條件都會(huì)使之失效

所以最終還是要看T被推導(dǎo)成什么類(lèi)型,如果T被推導(dǎo)成了string误褪,那么T&&就是string&&床未,是個(gè)右值引用,如果T被推導(dǎo)為string&振坚,就會(huì)發(fā)生類(lèi)似string& &&的情況薇搁,對(duì)于這種情況,c++11增加了引用折疊的規(guī)則渡八,總結(jié)如下:

  1. 所有的右值引用疊加到右值引用上仍然使一個(gè)右值引用啃洋。
  2. 所有的其他引用類(lèi)型之間的疊加都將變成左值引用传货。

如上面的T& &&其實(shí)就被折疊成了個(gè)string &,是一個(gè)左值引用宏娄。

#include <iostream>
#include <type_traits>
#include <string>
using namespace std;

template<typename T>
void f(T&& param){
    if (std::is_same<string, T>::value)
        std::cout << "string" << std::endl;
    else if (std::is_same<string&, T>::value)
        std::cout << "string&" << std::endl;
    else if (std::is_same<string&&, T>::value)
        std::cout << "string&&" << std::endl;
    else if (std::is_same<int, T>::value)
        std::cout << "int" << std::endl;
    else if (std::is_same<int&, T>::value)
        std::cout << "int&" << std::endl;
    else if (std::is_same<int&&, T>::value)
        std::cout << "int&&" << std::endl;
    else
        std::cout << "unkown" << std::endl;
}

int main()
{
    int x = 1;
    f(1); // 參數(shù)是右值 T推導(dǎo)成了int, 所以是int&& param, 右值引用
    f(x); // 參數(shù)是左值 T推導(dǎo)成了int&, 所以是int&&& param, 折疊成 int&,左值引用
    int && a = 2;
    f(a); //雖然a是右值引用问裕,但它還是一個(gè)左值, T推導(dǎo)成了int&
    string str = "hello";
    f(str); //參數(shù)是左值 T推導(dǎo)成了string&
    f(string("hello")); //參數(shù)是右值孵坚, T推導(dǎo)成了string
    f(std::move(str));//參數(shù)是右值粮宛, T推導(dǎo)成了string
}

所以,歸納一下卖宠, 傳遞左值進(jìn)去巍杈,就是左值引用,傳遞右值進(jìn)去扛伍,就是右值引用筷畦。如它的名字,這種類(lèi)型確實(shí)很"通用"刺洒,下面要講的完美轉(zhuǎn)發(fā)鳖宾,就利用了這個(gè)特性。

完美轉(zhuǎn)發(fā)

所謂轉(zhuǎn)發(fā)逆航,就是通過(guò)一個(gè)函數(shù)將參數(shù)繼續(xù)轉(zhuǎn)交給另一個(gè)函數(shù)進(jìn)行處理鼎文,原參數(shù)可能是右值,可能是左值因俐,如果還能繼續(xù)保持參數(shù)的原有特征拇惋,那么它就是完美的。

void process(int& i){
    cout << "process(int&):" << i << endl;
}
void process(int&& i){
    cout << "process(int&&):" << i << endl;
}

void myforward(int&& i){
    cout << "myforward(int&&):" << i << endl;
    process(i);
}

int main()
{
    int a = 0;
    process(a); //a被視為左值 process(int&):0
    process(1); //1被視為右值 process(int&&):1
    process(move(a)); //強(qiáng)制將a由左值改為右值 process(int&&):0
    myforward(2);  //右值經(jīng)過(guò)forward函數(shù)轉(zhuǎn)交給process函數(shù)女揭,卻稱(chēng)為了一個(gè)左值蚤假,
    //原因是該右值有了名字  所以是 process(int&):2
    myforward(move(a));  // 同上,在轉(zhuǎn)發(fā)的時(shí)候右值變成了左值  process(int&):0
    // forward(a) // 錯(cuò)誤用法吧兔,右值引用不接受左值
}

上面的例子就是不完美轉(zhuǎn)發(fā)磷仰,而c++中提供了一個(gè)std::forward()模板函數(shù)解決這個(gè)問(wèn)題。將上面的myforward()函數(shù)簡(jiǎn)單改寫(xiě)一下:

void myforward(int&& i){
    cout << "myforward(int&&):" << i << endl;
    process(std::forward<int>(i));
}

myforward(2); // process(int&&):2

上面修改過(guò)后還是不完美轉(zhuǎn)發(fā)境蔼,myforward()函數(shù)能夠?qū)⒂抑缔D(zhuǎn)發(fā)過(guò)去灶平,但是并不能夠轉(zhuǎn)發(fā)左值,解決辦法就是借助universal references通用引用類(lèi)型和std::forward()模板函數(shù)共同實(shí)現(xiàn)完美轉(zhuǎn)發(fā)箍土。例子如下:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

void RunCode(int &&m) {
    cout << "rvalue ref" << endl;
}
void RunCode(int &m) {
    cout << "lvalue ref" << endl;
}
void RunCode(const int &&m) {
    cout << "const rvalue ref" << endl;
}
void RunCode(const int &m) {
    cout << "const lvalue ref" << endl;
}

// 這里利用了universal references逢享,如果寫(xiě)T&,就不支持傳入右值,而寫(xiě)T&&吴藻,既能支持左值瞒爬,又能支持右值
template<typename T>
void perfectForward(T && t) {
    RunCode(forward<T> (t));
}

template<typename T>
void notPerfectForward(T && t) {
    RunCode(t);
}

int main()
{
    int a = 0;
    int b = 0;
    const int c = 0;
    const int d = 0;

    notPerfectForward(a); // lvalue ref
    notPerfectForward(move(b)); // lvalue ref
    notPerfectForward(c); // const lvalue ref
    notPerfectForward(move(d)); // const lvalue ref

    cout << endl;
    perfectForward(a); // lvalue ref
    perfectForward(move(b)); // rvalue ref
    perfectForward(c); // const lvalue ref
    perfectForward(move(d)); // const rvalue ref
}

上面的代碼測(cè)試結(jié)果表明,在universal referencesstd::forward的合作下,能夠完美的轉(zhuǎn)發(fā)這4種類(lèi)型侧但。

emplace_back減少內(nèi)存拷貝和移動(dòng)

我們之前使用vector一般都喜歡用push_back()矢空,由上文可知容易發(fā)生無(wú)謂的拷貝,解決辦法是為自己的類(lèi)增加移動(dòng)拷貝和賦值函數(shù)禀横,但其實(shí)還有更簡(jiǎn)單的辦法屁药!就是使用emplace_back()替換push_back(),如下面的例子:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class A {
public:
    A(int i){
//        cout << "A()" << endl;
        str = to_string(i);
    }
    ~A(){}
    A(const A& other): str(other.str){
        cout << "A&" << endl;
    }

public:
    string str;
};

int main()
{
    vector<A> vec;
    vec.reserve(10);
    for(int i=0;i<10;i++){
        vec.push_back(A(i)); //調(diào)用了10次拷貝構(gòu)造函數(shù)
//        vec.emplace_back(i);  //一次拷貝構(gòu)造函數(shù)都沒(méi)有調(diào)用過(guò)
    }
    for(int i=0;i<10;i++)
        cout << vec[i].str << endl;
}

可以看到效果是明顯的柏锄,雖然沒(méi)有測(cè)試時(shí)間酿箭,但是確實(shí)可以減少拷貝。emplace_back()可以直接通過(guò)構(gòu)造函數(shù)的參數(shù)構(gòu)造對(duì)象趾娃,但前提是要有對(duì)應(yīng)的構(gòu)造函數(shù)缭嫡。

對(duì)于mapset,可以使用emplace()茫舶⌒笛玻基本上emplace_back()對(duì)應(yīng)push_bakc(), emplce()對(duì)應(yīng)insert()刹淌。

移動(dòng)語(yǔ)義對(duì)swap()函數(shù)的影響也很大饶氏,之前實(shí)現(xiàn)swap可能需要三次內(nèi)存拷貝,而有了移動(dòng)語(yǔ)義后有勾,就可以實(shí)現(xiàn)高性能的交換函數(shù)了疹启。

template <typename T>
void swap(T& a, T& b)
{
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

如果T是可移動(dòng)的,那么整個(gè)操作會(huì)很高效蔼卡,如果不可移動(dòng)喊崖,那么就和普通的交換函數(shù)是一樣的,不會(huì)發(fā)生什么錯(cuò)誤雇逞,很安全荤懂。

總結(jié)

  • 由兩種值類(lèi)型,左值和右值塘砸。
  • 有三種引用類(lèi)型节仿,左值引用、右值引用和通用引用掉蔬。左值引用只能綁定左值廊宪,右值引用只能綁定右值,通用引用由初始化時(shí)綁定的值的類(lèi)型確定女轿。
  • 左值和右值是獨(dú)立于他們的類(lèi)型的箭启,右值引用可能是左值可能是右值,如果這個(gè)右值引用已經(jīng)被命名了蛉迹,他就是左值傅寡。
  • 引用折疊規(guī)則:所有的右值引用疊加到右值引用上仍然是一個(gè)右值引用,其他引用折疊都為左值引用。當(dāng)T&&為模板參數(shù)時(shí)荐操,輸入左值大猛,它將變成左值引用,輸入右值則變成具名的右值應(yīng)用淀零。
  • 移動(dòng)語(yǔ)義可以減少無(wú)謂的內(nèi)存拷貝挽绩,要想實(shí)現(xiàn)移動(dòng)語(yǔ)義,需要實(shí)現(xiàn)移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值函數(shù)驾中。
  • std::move()將一個(gè)左值轉(zhuǎn)換成一個(gè)右值唉堪,強(qiáng)制使用移動(dòng)拷貝和賦值函數(shù),這個(gè)函數(shù)本身并沒(méi)有對(duì)這個(gè)左值什么特殊操作肩民。
  • std::forward()universal references通用引用共同實(shí)現(xiàn)完美轉(zhuǎn)發(fā)唠亚。
  • empalce_back()替換push_back()增加性能。

TODO

  • 對(duì)模板類(lèi)型自動(dòng)推導(dǎo)還不太熟悉持痰,繼續(xù)學(xué)習(xí)Effective Modern C++灶搜。
  • std::move()和std::forward()好像實(shí)現(xiàn)的并不復(fù)雜,有機(jī)會(huì)弄明白實(shí)現(xiàn)原理工窍。

我的SegmentFault鏈接

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末割卖,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子患雏,更是在濱河造成了極大的恐慌鹏溯,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淹仑,死亡現(xiàn)場(chǎng)離奇詭異丙挽,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)匀借,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)颜阐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人吓肋,你說(shuō)我怎么就攤上這事凳怨。” “怎么了蓬坡?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵猿棉,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我屑咳,道長(zhǎng)萨赁,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任兆龙,我火速辦了婚禮杖爽,結(jié)果婚禮上敲董,老公的妹妹穿的比我還像新娘。我一直安慰自己慰安,他們只是感情好腋寨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著化焕,像睡著了一般萄窜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撒桨,一...
    開(kāi)封第一講書(shū)人閱讀 51,727評(píng)論 1 305
  • 那天查刻,我揣著相機(jī)與錄音,去河邊找鬼凤类。 笑死穗泵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谜疤。 我是一名探鬼主播佃延,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼夷磕!你這毒婦竟也來(lái)了履肃?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤企锌,失蹤者是張志新(化名)和其女友劉穎榆浓,沒(méi)想到半個(gè)月后于未,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體撕攒,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年烘浦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抖坪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闷叉,死狀恐怖擦俐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情握侧,我是刑警寧澤蚯瞧,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站品擎,受9級(jí)特大地震影響埋合,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜萄传,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一甚颂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦振诬、人聲如沸蹭睡。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肩豁。三九已至,卻和暖如春辫呻,著一層夾襖步出監(jiān)牢的瞬間蓖救,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工印屁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留循捺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓雄人,卻偏偏與公主長(zhǎng)得像从橘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子础钠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

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