一次性搞定右值,右值引用(&&),和move語義


英文版發表在hackernoon並在個人博客存檔。c++

本文是漢化重製版。bash

C++在性能和擴展性上越走越遠,結果犧牲了易用性,一版比一版更不易學習。這篇文章主要討論新版C++的幾個相關的知識,右值,右值引用(&&),和move語義,但願能幫助你一次搞定這幾個難點。函數

首先,咱們先來看看啥是

右值(r-value)

簡單點說,右值就是在等號右邊的值。
性能

打上碼:學習

int var; // too much JavaScript recently:)
var = 8; // OK! l-value (yes, there is a l-value) on the left
8 = var; // ERROR! r-value on the left
(var + 1) = 8; // ERROR! r-value on the left複製代碼


夠簡單吧。咱們看一個更隱晦的狀況,函數返回右值。

打上碼:ui

#include <string>
#include <stdio.h>

int g_var = 8;
int& returnALvalue() {
   return g_var; //here we return a left value
}

int returnARvalue() {
   return g_var; //here we return a r-value
}

int main() {
   printf("%d", returnALvalue()++); // g_var += 1;
   printf("%d", returnARvalue());
}複製代碼


結果:spa

8
9複製代碼


注意,我在例子裏函數返回左值只是爲了作演示,現實生活中請勿模仿。

指針

右值具體有啥用

其實,在右值引用(&&)發明以前,右值就已經能夠影響代碼邏輯了。

好比這行代碼:
c++11

const std::string& name = "rvalue";複製代碼

沒有問題,可是下面這行:code

std::string& name = "rvalue"; // use a left reference for a rvalue複製代碼

是編譯不過的:

error: non-const lvalue reference to type 'std::string' (aka 'basic_string<char, char_traits<char>, allocator<char> >') cannot bind to a value of unrelated type 'const char [7]'複製代碼

說明編譯器強制咱們用常量引用來指向右值。

再來個更有趣的🌰:

#include <stdio.h>
#include <string>

void print(const std::string& name) {
    printf("rvalue detected:%s\n", name.c_str());
}

void print(std::string& name) {
    printf("lvalue detected:%s\n", name.c_str());
}

int main() {
    std::string name = "lvalue";
    print(name); //compiler can detect the right function for lvalue
    print("rvalue"); // likewise for rvalue
}
複製代碼

運行結果:

lvalue detected:lvalue
rvalue detected:rvalue複製代碼

說明這個差別足以讓編譯器決定重載函數。

因此右值就是常量咯?

不徹底是。這時就輪到&&出場了。

打上碼:

#include <stdio.h>
#include <string>

void print(const std::string& name) {
  printf(「const value detected:%s\n」, name.c_str());
}

void print(std::string& name) {
  printf(「lvalue detected%s\n」, name.c_str());
}

void print(std::string&& name) {
  printf(「rvalue detected:%s\n」, name.c_str());
}

int main() {
  std::string name = 「lvalue」;
  const std::string cname = 「cvalue」;

  print(name);
  print(cname);
  print(「rvalue」);
}複製代碼

運行結果:

lvalue detected:lvalue
const value detected:cvalue
rvalue detected:rvalue複製代碼

說明若是有專門爲右值重載的函數的時候,右值的傳參會去選擇專有函數(接受&&參數的那個),而不去選更通用的接受常量引用做爲參數的函數。因此,&&能夠更加細化右值和常量引用。

我總結了函數實參(實際傳的那個變量)和形參(括號裏聲明的那個變量)的適配性,有興趣的話你也能夠經過改上面的🌰驗證下:

把常量引用細分紅常量引用和右值是很好,可是仍是沒回答具體有啥用。

&&解決了什麼問題?

問題是當參數爲右值時,沒必要要的深拷貝。

講具體點,&&用來區分右值,這樣在這個右值 1)是一個構造函數或賦值函數的參數,和2)對應的類包含指針,並指向一個動態分配的資源(內存)時,就能夠在函數內避免深拷貝。

用代碼的話還能夠具體點:

#include <stdio.h>
#include <string>
#include <algorithm>

using namespace std;

class ResourceOwner {
public:
  ResourceOwner(const char res[]) {
    theResource = new string(res);
  }

  ResourceOwner(const ResourceOwner& other) {
    printf("copy %s\n", other.theResource->c_str());
    theResource = new string(other.theResource->c_str());
  }

  ResourceOwner& operator=(const ResourceOwner& other) {
    ResourceOwner tmp(other);
    swap(theResource, tmp.theResource);
    printf("assign %s\n", other.theResource->c_str());
  }

  ~ResourceOwner() {
    if (theResource) {
      printf("destructor %s\n", theResource->c_str());
      delete theResource;
    }
  }
private:
  string* theResource;
};

void testCopy() {
 // case 1
  printf("=====start testCopy()=====\n");
  ResourceOwner res1("res1");
  ResourceOwner res2 = res1;
  //copy res1
  printf("=====destructors for stack vars, ignore=====\n");
}

void testAssign() {
 // case 2
  printf("=====start testAssign()=====\n");
  ResourceOwner res1("res1");
  ResourceOwner res2("res2");
  res2 = res1;
 //copy res1, assign res1, destrctor res2
  printf("=====destructors for stack vars, ignore=====\n");
}

void testRValue() {
 // case 3
  printf("=====start testRValue()=====\n");
  ResourceOwner res2("res2");
  res2 = ResourceOwner("res1");
 //copy res1, assign res1, destructor res2, destructor res1
  printf("=====destructors for stack vars, ignore=====\n");
}

int main() {
  testCopy();
  testAssign();
  testRValue();
}複製代碼

運行結果:

=====start testCopy()=====copy res1=====destructors for stack vars, ignore=====destructor res1destructor res1=====start testAssign()=====copy res1assign res1destructor res2=====destructors for stack vars, ignore=====destructor res1destructor res1=====start testRValue()=====copy res1assign res1destructor res2destructor res1=====destructors for stack vars, ignore=====destructor res1複製代碼

前兩個例子testCopy()testAssign()裏面的結果沒問題。這裏將res1裏面的的資源拷貝到res2裏是合理的,由於這兩個獨立的個體都須要有各自的獨享資源(string)。

可是在第三個例子就不對了。此次深拷貝的對象res1是個右值(ResourceOwner(「res1」)的返回值),其實它立刻就要被回收了。因此自己是不須要獨享資源的。

我把問題描述再重複一次,此次應該就好理解了:

&&用來區分右值,這樣在這個右值 1)是一個構造函數或賦值函數的參數,和2)對應的類包含指針,並指向一個動態分配的資源(內存)時,就能夠在函數內避免深拷貝。

若是深拷貝右值的資源不合理,那什麼操做是合理的呢?答案是

Move

繼續討論move語義。解決方法很簡單,若是參數是右值,就不拷貝,而是直接「搬」資源。咱們先把賦值函數用右值引用重載下:

ResourceOwner& operator=(ResourceOwner&& other) {
  theResource = other.theResource;
  other.theResource = NULL;
}複製代碼

這個新的賦值函數就叫作move賦值函數move構造函數也能夠用差很少的辦法實現,這裏先不贅述了。

若是不太好理解的話,能夠這麼來:好比你賣了箇舊房子搬新家,搬家的時候不必定要把傢俱都丟掉再買新的對伐(咱們在🌰3裏面就丟掉了)。你也能夠把傢俱「搬」到新家去。

完美。

那std::move又是啥?

最後咱們來解決這個std::move。

咱們仍是先看看問題:

當1)咱們知道一個參數是右值,可是2)編譯器不知道的時候,這個參數是調不到move重載函數的。

一個常見的狀況是在resource owner上面再加一層類ResourceHolder

holder
 |
 |----->owner
         |
         |----->resource複製代碼

注意,在下面的代碼裏,我把move構造函數也加上了。

打上碼:

#include <string>
#include <algorithm>


using namespace std;

class ResourceOwner {
public:
  ResourceOwner(const char res[]) {
    theResource = new string(res);
  }


  ResourceOwner(const ResourceOwner& other) {
    printf(「copy %s\n」, other.theResource->c_str());
    theResource = new string(other.theResource->c_str());
  }

++ResourceOwner(ResourceOwner&& other) {
++ printf(「move cons %s\n」, other.theResource->c_str());
++ theResource = other.theResource;
++ other.theResource = NULL;
++}


  ResourceOwner& operator=(const ResourceOwner& other) {
    ResourceOwner tmp(other);
    swap(theResource, tmp.theResource);
    printf(「assign %s\n」, other.theResource->c_str());
  }


++ResourceOwner& operator=(ResourceOwner&& other) {
++ printf(「move assign %s\n」, other.theResource->c_str());
++ theResource = other.theResource;
++ other.theResource = NULL;
++}

  ~ResourceOwner() {
    if (theResource) {
      printf(「destructor %s\n」, theResource->c_str());
      delete theResource;
    }
  }

private:
  string* theResource;
};


class ResourceHolder {
……
ResourceHolder& operator=(ResourceHolder&& other) {
  printf(「move assign %s\n」, other.theResource->c_str());
  resOwner = other.resOwner;
}
……
private:
  ResourceOwner resOwner;
}複製代碼

ResourceHoldermove賦值函數中,其實咱們想調用的是的move賦值函數,由於右值的成員也是右值。可是

resOwner = other.resOwner

實際上是調用了普通賦值函數,仍是作了深拷貝。

那再重複一次問題,看看是否是好理解了:

當1)咱們知道一個參數是右值,可是2)編譯器不知道的時候,這個參數是調不到move重載函數的。

解決方法是,咱們能夠用std::move把這個變量強制轉化成右值,就能調用到正確的重載函數了。

ResourceHolder& operator=(ResourceHolder&& other) {
  printf(「move assign %s\n」, other.theResource->c_str());
  resOwner = std::move(other.resOwner);
}複製代碼

還能再深刻一點嗎?

徹底能夠!

咱們都知道強轉除了讓編譯器閉嘴,實際上是會生成對應的機器碼的。(在不開O的狀況下比較容易觀察到)這些機器碼會把變量在不一樣大小的寄存器裏面移來移去來真正完成強轉操做。

因此std::move也和強轉作了相似的操做嗎?我也不知道,一塊兒來試試看。

首先,咱們把main函數改一改,(我儘可能保持邏輯一致)

打上碼:

int main() {
  ResourceOwner res(「res1」);
  asm(「nop」); // remeber me
  ResourceOwner && rvalue = std::move(res);
  asm(「nop」); // remeber me
}複製代碼

編譯它,而後用下面的命令把彙編語言打出來

clang++ -g -c -std=c++11 -stdlib=libc++ -Weverything move.cc
gobjdump -d -D move.o複製代碼

😯,原來藏在下面的畫風是這樣的:

0000000000000000 <_main>:
 0: 55 push %rbp
 1: 48 89 e5 mov %rsp,%rbp
 4: 48 83 ec 20 sub $0x20,%rsp
 8: 48 8d 7d f0 lea -0x10(%rbp),%rdi
 c: 48 8d 35 41 03 00 00 lea 0x341(%rip),%rsi # 354
 <GCC_except_table5+0x18>
 13: e8 00 00 00 00 callq
 18 <_main+0x18> 18: 90 nop // remember me
 19: 48 8d 75 f0 lea -0x10(%rbp),%rsi
 1d: 48 89 75 f8 mov %rsi,-0x8(%rbp)
 21: 48 8b 75 f8 mov -0x8(%rbp),%rsi
 25: 48 89 75 e8 mov %rsi,-0x18(%rbp)
 29: 90 nop // remember me
 2a: 48 8d 7d f0 lea -0x10(%rbp),%rdi
 2e: e8 00 00 00 00 callq 33 <_main+0x33>
 33: 31 c0 xor %eax,%eax
 35: 48 83 c4 20 add $0x20,%rsp
 39: 5d pop %rbp
 3a: c3 retq
 3b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)複製代碼

我也看不太懂,還好用nop作了染色。看兩個nop中間那段確實生成了一些機器碼,可是這些機器碼貌似啥都沒作,只是簡單的把一個變量的地址賦值給另外一個而已。而且,若是咱們把O(-O1就夠了)打開,全部的nop中間的機器碼就都被幹掉了。

clang++ -g -c -O1 -std=c++11 -stdlib=libc++ -Weverything move.cc
gobjdump -d -D move.o複製代碼

再來,若是把關鍵行改爲

ResourceOwner & rvalue = res;複製代碼

除了變量的相對偏移有變化,其實生成的機器碼是同樣同樣的。

說明了在這裏std::move實際上是個純語法糖,而並無啥實際的操做。



好了,今天先寫到這裏。若是你喜歡本篇,歡迎點贊和關注。有興趣也能夠去Medium上隨意啪啪啪個人其餘文章。感謝閱讀。

相關文章
相關標籤/搜索