帶你一起探索 c++11 中右值引用褥影、移動構造、&&咏雌、move凡怎、forward

本文將介紹帶你一步步的了解 c++11 中:

  • 右值校焦、右值引用
  • 移動構造函數
  • && 解密
  • move 移動語義
  • forward 完美轉發(fā)

產生原由

class Object
 {
 public:    
     //無參構造函數   
     Object() : m_num(new int(10))    
     {        
         std::cout << "contr function..." << std::endl;        
         printf("m_num 地址:%p\n", m_num);    
     }        
     //拷貝構造函數    
     Object(const Object& o) : m_num(new int(*o.m_num))    
     {       
         std::cout << "copy contr function..." << std::endl;   
     }       
 private:    
     int* m_num;
 };

我們知道,某個類中如果含有指向堆內存的(一般含有指針)成員變量栅贴,如果不編寫拷貝構造函數斟湃,那么編譯器將調用默認的拷貝構造函數,即只進行淺拷貝檐薯。即下圖中只拷貝了 a 指針,將會出現 a注暗、b指向同一塊內存坛缕,為了防止堆區(qū)地址雙重釋放,那么應該編寫 拷貝構造函數 防止淺拷貝發(fā)生問題捆昏。即在拷貝構造函數中重新分配一塊內存進行初始化赚楚。

1616898818316.png

問題:在 c++11 中引入了右值引用和移動構造函數又是為了什么呢?

下面的代碼中調用 getObj 函數初始化一個對象 oo1, 分析其執(zhí)行過程:

1骗卜、getObj 函數中初始化一個臨時對象 temp宠页, 調用構造函數;

2寇仓、將臨時對象賦值給 oo1举户, 調用拷貝構造函數;

class Object
{
    ...
}

Object getObj() {
    //1遍烦、初始化一個臨時對象 temp俭嘁, 調用構造函數;
    Object temp;
    return temp;
}

int main() {
    //2服猪、將臨時對象賦值給 `oo1`供填, 調用拷貝構造函數;
    Object oo1 = getObj();
    return 0;
};

執(zhí)行結果:

contr function...          
m_num 地址:00E7F6D8
copy contr function...

根據執(zhí)行結果罢猪,與預期一致近她。

問題: 如果在第一步中,在調用 getObj時膳帕,創(chuàng)建的臨時對象 temp 在構造過程中如果要進行大量的初始化工作(特別耗時)粘捎,并且其用完后將被釋放; 第二步中备闲,將臨時對象拷貝給 oo1 時晌端,也需要進行大量的拷貝工作。oo1 在生命周期結束后也將釋放恬砂。

思考: 中間產生了臨時對象 temp 只起到賦值作用咧纠,極大的耗費性能。有什么方式可以避免產生這個中間的臨時變量呢泻骤?怎么去優(yōu)化它呢漆羔?

答案: c++ 11 的右值引用梧奢。

右 值

c++11 中引入了右值的概念,使用 && 標記演痒。

不必要去記其概念亲轨,只需要知道怎么去判別即可,可以被取地址的即為左值鸟顺,反之為右值惦蚊。

int x = 1000;
int y = 2000;
x = y;

其中, 等號左邊的 x讯嫂、y 可以取地址蹦锋,即為左值; 等號右邊 1000欧芽、 2000 為右值莉掂; 處于等號左右的 x = y 中,因為其都可以進行取地址千扔,所以都為左值憎妙。

右值引用

右值引用也即是一個引用,和左值引用一樣曲楚,只不過左值引用是左值的別名厘唾。右值不具備名字,所以只能使用右值引用標記它洞渤。因為左值引用和右值引用都是別名阅嘶,不擁有所綁定對象的內存,所以必須進行初始化操作载迄。右值被右值引用接收后重新有了名字讯柔,只要該引用變量存活,护昧,右值也將存活魂迄。即右值引用可以延長某塊內存的存活時間。

int&& data = 1000;  //必須進行初始化

class Object
{
public:
    Object()
    {
        std::cout << "contr function..." << std::endl;
    }
    Object(const Test& a)
    {
        std::cout << "copy contr function..." << std::endl;
    }
};

Object getObj()
{
    return Object();
}

int main()
{
    int a1;
    int &&a2 = a1;        // error
    Object& t = getObj();   // error
    Object && t = getObj();
    const Object& t = getObj();
    return 0;
}

  • int &&a2 = a1; a1 具有名字惋耙,其為左值捣炬,左值賦值給右值引用 錯誤
  • Object& t = getObj(); getObj 函數返回一個沒有名字的右值,將一個右值賦值給左值引用 錯誤
  • Object && t = getObj(); 右值賦值給右值引用 正確
  • const Object& t = getObj(); 常量左值引用被成為萬能引用绽榛,既可以引用左值也可以引用右值 正確

性能優(yōu)化

介紹完右值和右值引用湿酸, 在回到上面的問題:中間產生了臨時對象temp只起到賦值作用,極大的耗費性能灭美。有什么方式可以避免產生這個中間的臨時變量呢推溃?怎么去優(yōu)化它呢?

class Object
{
    ...
}

Object getObj() {
    //1届腐、初始化一個臨時對象 temp铁坎, 調用構造函數蜂奸;
    Object temp;
    return temp;
}

int main() {
    //2、將臨時對象賦值給 `oo1`硬萍, 調用拷貝構造函數扩所;
    Object oo1 = getObj();
    return 0;
};

getObj 函數中創(chuàng)建的臨時對象(堆上)構建完成后,還沒有使用朴乖,就釋放掉了祖屏,那么如果可以復用這個臨時對象,將會對性能有很大幫助寒砖。

那么如何復用呢赐劣? 給該類編寫移動構造函數即可。

class Object
{
    ...
    //移動構造函數
    Object(Object&& o) {
        m_num = o.m_num;
        o.m_num = nullptr;
        std::cout << "move contr function..." << std::endl;
    }

private:
    int* m_num;
}

Object getObj() {
    //1哩都、初始化一個臨時對象 temp, 調用構造函數婉徘;
    Object temp;
    return temp;
}

int main() {
    //2漠嵌、 調用移動構造函數
    Object oo1 = getObj();
    return 0;
};

執(zhí)行結果:

contr function...
m_num 地址:00CCF5B0
move contr function...

執(zhí)行結果調用了移動構造函數。那么我們分析移動構造函數中發(fā)生了什么盖呼?

 //移動構造函數
Object(Object&& o) {
    m_num = o.m_num;
    o.m_num = nullptr;
    std::cout << "move contr function..." << std::endl;
}
...

Object oo1 = getObj();

因為臨時對象用完就釋放儒鹿,白白構造那么長時間。在執(zhí)行 Object oo1 = getObj(); 這條語句是几晤,調用移動構造函數將臨時對象 temp 所指的內存直接賦值給 oo1 對象的指針(m_num = o.m_num;); 然后避免 temp 出了作用域銷毀內存约炎,則將 temp 指向的內存置空(o.m_num = nullptr;)。 oo1 對象直接擁有了 構造 temp 時分配的內存蟹瘾。

1616903425781.png

上圖充分的展示了拷貝構造函數和移動構造函數的關系圾浅,可以看出,移動構造函數整體上少分配了一塊內存憾朴,相當于淺拷貝狸捕,只不過最后將原始指針置空,因此極大的節(jié)省了空間和時間众雷。

問題1: 什么時候會調用移動構造函數灸拍?

答案: 要求右側的對象是一個臨時對象,才會調用移動構造函數砾省,如果沒有移動構造函數鸡岗,則將調用拷貝構造函數。因此可以看出编兄,移動構造函數不是必須存在的轩性,只是為了性能優(yōu)化而存在的。

可以將上述代碼中移動構造函數注釋掉翻诉,編譯器將會調用拷貝構造函數炮姨。

問題2: 怎么編寫移動構造函數呢捌刮?

從上面可以看出,移動構造函數實質是為了復用其他對象的資源而產生的舒岸,這種資源往往是堆內存的資源绅作。那么在編寫移動構造函數過程中,只需要轉移該類中關于堆上的資源即可蛾派。

右值符號 && 解密

在很多代碼模板函數中經常會出現諸如以下的代碼:

1俄认、 static_cast<typename remove_reference<_Ty>::type&&>(_Arg)

2、 typename<class T> void function(T&& t)

代碼中的 && 會不會讓你暈頭轉向洪乍? 如果是眯杏,那么和我一起解密吧!

c++ 中有一種叫做未定義的引用類型壳澳,通常有以下兩種方式:

自動類型推導的  auto&&
模板類型推導的  T&&

有一種特列 const T&&岂贩, 表示右值引用,不屬于未定義類型引用巷波。

那么接下來記住兩個規(guī)則即可萎津,不對,是一個規(guī)則(引用折疊):

使用右值推導 T&& 和 auto&& 得到的是一個右值引用類型抹镊;其他的都是左值引用類型锉屈。

int a = 10;
int b = 250;
auto&& x = a;  //a 是一個左值, auto&& 表示左值引用
auto&& y = 100;  //100 是右值垮耳, auto&& 表示右值引用

int&& a1 = 5; 
auto&& b1 = a1; //a1是右值引用颈渊,不是右值,所以b1 是左值引用

int a2 = 10;  
int& a3 = a2;   //a2是左值终佛,a3為左值引用
auto&& c1 = a3; // a3是左值引用俊嗽, c1 即是左值引用類型
auto&& c2 = a2; // a2是左值, c2 即是左值引用類型

const int& d1 =3;  
const int&& d2 = 4;
auto&& e1 = d1;  //d1是常量左值引用查蓉, e1 即為常量左值引用
auto&& e2 = d2;  //d2是常量右值引用乌询, e2 即為常量左值引用

通過以上既可以理解,使用右值推導 T&& 和 auto&& 得到的都是右值引用類型豌研,其他的都是左值引用類型妹田。

void printX(int &x)
{
    std::cout << "l-value: " << x << std::endl;
}

void printX(int &&x)
{
    cout << "r-value: " << x << endl;
}

void forward(int &&x)
{
    printX(x);
}

int main()
{
    int a = 100;
    printX(a);
    printX(20);
    forward(500);

    return 0;
    system("pause");
};

上面定義了兩個重載函數 printX, 先對上面的輸出結果進行推導:

1、 printX(a); 其中 a 是一個左值鹃共,那么調用第一個 printX函數鬼佣,輸出應該是左值;

2霜浴、 printX(20); 其中 20 是右值晶衷, 那么調用第二個printX函數, 輸出應該是右值;

3晌纫、 forward(500), 其中 500 是右值税迷,forward 形參x 是右值引用類型,繼續(xù)調用printX锹漱,由于此時的右值具備的名字箭养,所以將退化成一個左值,所以將調用第一個 printX 函數哥牍, 輸出應該是左值毕泌;

對于最后以重情況可能稍微難以理解,只需要記住嗅辣,右值引用在傳遞的過程中將會退化成左值引用撼泛。這也是 std::forward 為了防止退化成左值引用,所以才出現的澡谭,被譽為完美轉發(fā) 愿题,即不做任何變動的轉發(fā)。將上述 forward 函數中的 printX(x)改成 printX(std::forward<int>(x)) 將會調用第二個函數蛙奖,輸出應該是右值抠忘。

輸出結果:

l-value: 100
r-value: 20
l-value: 500

結果完全正確。

std::move

在上述的例子中外永,有一種情況是不能進行賦值的,即用一個左值初始化一個右值引用拧咳;

int a = 10;
int&& b = a;  //error 無法將右值引用綁定到左值

所以 std::move 函數應運而生伯顶, move 通常被理解為“移動”, 但在本文中被譯為“轉移”更加合適骆膝,即轉移所有權祭衩,將你的房產名字轉給你的老婆,房子本身不變阅签,只是所有者發(fā)生了變化掐暮。

再來看 std::move 的源代碼:

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

使用 std::move 后,上述代碼可以更改為:

int a = 10;
int&& b = std::move(a);  //ok 

對于這種將左值轉化為右值的方法有什么用處呢政钟?

vector<string> vec;
vec.push_back("wang");
vec.push_back("zhuo");
.....
//插入一百萬條數據
.....
vector<string> vec1 = vec;
vector<string> vec2 = std::move(vec);

如果用 vec 這個左值直接初始化 vec1路克,將會發(fā)生大量的內存拷貝。y

如果用 vec2 = std::move(vec), 直接將 vec 的所有權轉移給 vec2即可

用處养交? 在對于擁有大量的堆內存或者動態(tài)數組時候精算,使用 std::move 可以有效的節(jié)省時間效率。

如果將 std::move 和 移動構造函數結合起來碎连,盡可能重復利用資源灰羽, 移動構造函數接收的是一個右值引用類型。

std::forward

上文也提及到了完美轉發(fā) forward,即在右值引用傳遞的過程中廉嚼,為了防止被編譯器當作左值處理玫镐,使其以原由類型進行轉發(fā),引入了 forward怠噪。

原型如下:

std::forward<T>(x);

當forward 的模板類型參數 T 為左值引用類型時恐似,x將被轉換成 T類型的左值,否則將被轉換成右值舰绘。

template<typename T>
void printX(T& t)
{
    std::cout << "left value"<< std::endl;
}

template<typename T>
void printX(T&& t)
{
    std::cout << "rifht value " <<std::endl;
}

template<typename T>
void test(T && v)
{
    printX(v);
    printX(move(v));
    printX(forward<T>(v));
}

int main()
{
    test(100);
    int num = 10;
    test(num);
    test(forward<int>(num));
    test(forward<int&>(num));
    test(forward<int&&>(num));

    return 0;
}

1蹂喻、test(100), 100 是右值,test形參是未定義引用類型捂寿,即根據上文提到的 ”使用右值推導 T&&auto&& 得到的是一個右值引用類型“ 形參 v 是一個具有名字的右值引用口四,但編譯器將其視為左值。

    * 傳遞給第一個函數 `printX `變?yōu)樽笾登芈{用第一個蔓彩,輸出左值。
    * 使用`move(v)` 之后驳概,左值 v 被 move 成右值赤嚼,輸出右值。
    * `printX(forward<T>(v))`顺又, T為右值引用更卒,因此最終將會成為右值, 輸出右值

2稚照、test(forward<int&>(num)); 模板參數為int&蹂空, 根據 ”當forward 的模板類型參數 T 為左值引用類型時,x將被轉換成 T類型的左值“ 果录, 將會得到一個左值上枕,test 形參為未定義類型 T&&, 根據 ”使用右值推導 T&&auto&& 得到的是一個右值引用類型弱恒,反之為左值引用“辨萍,即test 形參為左值引用類型。

* `printX(v);`  v 是左值返弹, 輸出左值
* `printX(move(v));` 左值經過 `move` 后成為右值锈玉,輸出右值
* `printX(forward<T>(v));` T類型為 `int&` ,v將被轉換成左值琉苇, 輸出左值嘲玫。

你學會了嗎? 其他的幾種留給你自己分析并扇。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末去团,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌土陪,老刑警劉巖昼汗,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異鬼雀,居然都是意外死亡顷窒,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門源哩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鞋吉,“玉大人,你說我怎么就攤上這事励烦∥阶牛” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵坛掠,是天一觀的道長赊锚。 經常有香客問我,道長屉栓,這世上最難降的妖魔是什么舷蒲? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任碗淌,我火速辦了婚禮惨恭,結果婚禮上,老公的妹妹穿的比我還像新娘罐寨。我一直安慰自己域滥,他們只是感情好欠拾,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著骗绕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪资昧。 梳的紋絲不亂的頭發(fā)上酬土,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天,我揣著相機與錄音格带,去河邊找鬼撤缴。 笑死,一個胖子當著我的面吹牛叽唱,可吹牛的內容都是我干的屈呕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼棺亭,長吁一口氣:“原來是場噩夢啊……” “哼虎眨!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嗽桩,失蹤者是張志新(化名)和其女友劉穎岳守,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體碌冶,經...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡湿痢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了扑庞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片譬重。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖罐氨,靈堂內的尸體忽然破棺而出臀规,到底是詐尸還是另有隱情,我是刑警寧澤岂昭,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布以现,位于F島的核電站,受9級特大地震影響约啊,放射性物質發(fā)生泄漏邑遏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一恰矩、第九天 我趴在偏房一處隱蔽的房頂上張望记盒。 院中可真熱鬧,春花似錦外傅、人聲如沸纪吮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碾盟。三九已至,卻和暖如春技竟,著一層夾襖步出監(jiān)牢的瞬間冰肴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工榔组, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留熙尉,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓搓扯,卻偏偏與公主長得像检痰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子锨推,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內容