【c++工程實踐】值語義與右值引用

1、值語義html

值語義(value sematics)指的是對象的拷貝與原對象無關,C++ 的內置類型(bool/int/double/char)都是值語義,標準庫裏的 complex<> 、pair<>、vector<>、map<>、string 等等類型也都是值語意,拷貝以後就與原對象脫離關係。對一個具備值語義的原始變量變量賦值能夠轉換成內存的bit-wise-copy數組

對於用戶定義的類,若是一個type X 具備值語義, 則:安全

1)X 的size在編譯時能夠肯定。函數

2)將X的變量x,賦值與另外一個變量y,無須專門的 = operator,簡單的bit-wise-copy 便可。spa

3)當上述賦值發生後,x和y脫離關係:x 和 y 能夠獨立銷燬, 其內存也能夠獨立釋放。指針

 1 瞭解第三點很重要,好比下面的class A就不具有值語義:
 2 class A
 3 {
 4      char * p;
 5      public:
 6           A() { p = new char[10]; }
 7           ~A() { delete [] p; }
 8 };
 9 A 知足1和2,但不知足3。由於下面的程序會出錯誤: 
10 Foo()
11 {
12     A a;
13     A b = a;
14 } // crash here

與值語義對應的是「對象語義/object sematics」,或者叫作引用語義(reference sematics),引用語義指的是面向對象意義下的對象,對象拷貝是禁止的。拷貝 TcpConnection 對象也沒有意義,系統裏邊只有一個 TCP 鏈接,拷貝 TcpConnection  對象不會讓咱們擁有兩個鏈接。code

C++ 要求凡是能放入標準容器的類型必須具備值語義。準確地說:type 必須是 SGIAssignable concept 的 model。可是,由 於C++ 編譯器會爲 class 默認提供 copy constructor 和 assignment operator,所以除非明確禁止,不然 class 老是能夠做爲標準庫的元素類型——儘管程序能夠編譯經過,可是隱藏了資源管理方面的 bug。所以,在寫一個 class 的時候,先讓它繼承 boost::noncopyable,幾乎老是正確的。htm

在現代 C++ 中,通常不須要本身編寫 copy constructor 或 assignment operator,由於只要每一個數據成員都具備值語義的話,編譯器自動生成的 member-wise copying&assigning 就能正常工做;若是以 smart ptr 爲成員來持有其餘對象,那麼就能自動啓用或禁用 copying&assigning對象

t2 = t1;  // calls assignment operator, same as "t2.operator=(t1);"
Test t3 = t1;  // calls copy constructor, same as "Test t3(t1);"

 值語義用於控制對象的生命期,而其具體的控制方式分爲兩種:blog

    • 生命期限於scope內:無需控制,到期自動調用析構函數。
    • 須要延長到scope外:移動語義。

由於右值引用的目的在於實現移動語義,因此右值引用 意義便是增強了值語義對對象生命期的控制能力。

2、左值、右值

左值對應變量的存儲位置,而右值對應變量的值自己。左值與右值的根本區別在因而否容許取地址&運算符得到對應的內存地址.

C++03及之前的標準定義了在表達式中左值到右值的三類隱式自動轉換:

  • 左值轉化爲右值;如整型變量i在表達式 (i+3)
  • 數組名是常量左值,在表達式[注 5]中轉化爲數組首元素的地址值
  • 函數名是常量左值,在表達式中轉化爲函數的地址值

不嚴格的來講,左值對應變量的存儲位置,而右值對應變量的值自己。

3、右值引用

3.1 左值引用

引用底層是用const指針實現的,分配額外的內存空間。

準確地說

1 int b=0;
2 int &a=b;

這種狀況等同於

1 int b=0;
2 int *const lambda=&b;
3 //此後 *lambda就徹底等價於上面的a

經過簡單的例子:

 1 int test_lvalue() {
 2   int b = 0;
 3   int& rb = b;
 4   rb = 1;
 5 
 6   return b;
 7 }
 8 
 9 int test_pointer() {
10   int b = 0;
11   int* pb = &b; 
12   *pb = 1;
13 
14   return b;
15 }

=> g++ -g -O0 test.cc

=> objdump -d -S a.out > test.i

 1 int test_pointer() {
 2   400876: 55                    push   %rbp
 3   400877: 48 89 e5              mov    %rsp,%rbp
 4   int b = 0;
 5   40087a: c7 45 f4 00 00 00 00  movl   $0x0,-0xc(%rbp)
 6   int* pb = &b; 
 7   400881: 48 8d 45 f4           lea    -0xc(%rbp),%rax
 8   400885: 48 89 45 f8           mov    %rax,-0x8(%rbp)
 9   *pb = 1;
10   400889: 48 8b 45 f8           mov    -0x8(%rbp),%rax
11   40088d: c7 00 01 00 00 00     movl   $0x1,(%rax)
12 
13   return b;
14   400893: 8b 45 f4              mov    -0xc(%rbp),%eax
15 }
16   400896: c9                    leaveq 
17   400897: c3                    retq  
 1 int test_lvalue() {
 2   400854: 55                    push   %rbp
 3   400855: 48 89 e5              mov    %rsp,%rbp
 4   int b = 0;
 5   400858: c7 45 f4 00 00 00 00  movl   $0x0,-0xc(%rbp)
 6   int& rb = b;
 7   40085f: 48 8d 45 f4           lea    -0xc(%rbp),%rax
 8   400863: 48 89 45 f8           mov    %rax,-0x8(%rbp)
 9   rb = 1;
10   400867: 48 8b 45 f8           mov    -0x8(%rbp),%rax
11   40086b: c7 00 01 00 00 00     movl   $0x1,(%rax)
12 
13   return b;
14   400871: 8b 45 f4              mov    -0xc(%rbp),%eax
15 }
16   400874: c9                    leaveq
17   400875: c3                    retq   

經過彙編指令,咱們也能夠看到,左值引用和指針在彙編層面是一致的。

 

3.2 右值引用

C++中右值能夠被賦值給左值或者綁定到引用。類的右值是一個臨時對象,若是沒有被綁定到引用,在表達式結束時就會被廢棄。因而咱們能夠在右值被廢棄以前,移走它的資源進行廢物利用,從而避免無心義的複製。被移走資源的右值在廢棄時已經成爲空殼,析構的開銷也會下降。

右值中的數據能夠被安全移走這一特性使得右值被用來表達移動語義。以同類型的右值構造對象時,須要以引用形式傳入參數。右值引用顧名思義專門用來引用右值,左值引用和右值引用能夠被分別重載,這樣確保左值和右值分別調用到拷貝和移動的兩種語義實現。對於左值,若是咱們明確放棄對其資源的全部權,則能夠經過std::move()來將其轉爲右值引用。

std::move

std::move()其實是static_cast<T&&>()的簡單封裝。

1 template <typename T>
2 decltype(auto) move(T&& param)
3 {
4    using return_type = std::remove_reference<T>::type&&;
5    return static_cast<return_type>(param);
6 }

咱們能夠看見這裏面的邏輯實際上是不管你的param是何種類型,都會被強制轉爲右值引用類型。

這裏須要注意的是模版這裏的T&& 類型,我願意欣賞與接受Meyers的叫法,他把這樣的類型叫作Universal Reference。對於Universal Reference來講,若你傳遞的param是一個左值,那麼T將會被deduce成Lvalue Reference(左值引用),其Param Type也是左值引用。若你傳遞進來的param是右值,那麼T則是正常的param類型,如int等,其Param Type結果是T&&。

舉一個簡單的栗子

1 template<typename T>
2 void foo(T&& param);
3 
4 int i = 7;
5 foo(i);
6 foo(47);

i是一個左值,因而T被deduce成int&,因而變爲了

1 foo(int& &&);

而整個參數的結果類型,即Param Type爲int&,C++不容許reference to reference,會進行引用摺疊,其規則爲:

1.當類型推導時可能會間接地建立引用的引用,此時必須進行引用摺疊。具體摺疊規則以下:

    A. X& &、X& &&和X&& &都摺疊成類型X&。即凡有左值引用參與的狀況下,最終的類型都會變成左值引用。

    B. 類型X&& &&摺疊成X&&。即只有所有爲右值引用的狀況纔會摺疊爲右值引用。

2.引用摺疊規則暗示咱們,能夠將任意類型的實參傳遞給T&&類型的函數模板參數。

而對於foo(47),因爲47是右值,那麼T被正常的deduce成int,因而變爲了

1 foo(int &&);

 

std::forward

對於forward,其boost的實現基本能夠等價於這樣的形式:

1 template <typename T>
2 T&& forward(typename remove_reference<T>::type& param)
3 {
4     return static_cast<T&&>(param);
5 }

那麼這裏面是如何達到完美轉發的呢?

舉一個栗子

1 template<typename T>
2 void foo(T&& fparam)
3 {
4     std::forward<T>(fparam);
5 }
6 
7 int i = 7;
8 foo(i);
9 foo(47);

如上文所述,這裏的i是一個左值,因而,咱們在void foo(T&& fparam)這裏的話,T將會被deduce成int& 而後Param Type爲int&。(注意,我這裏使用的變量名字爲fparam,以便與forward的param進行區分)

那麼爲何Param Type會是int&呢?由於按照正常的deduce,咱們將會獲得

1 void foo(int& &&fparam);

先前我簡單的提到了一句,C++不容許reference to reference,然而事實上,咱們卻會出現

Lvalue reference to Rvalue reference[1],

Lvalue reference to Lvalue reference[2],

Rvalue reference to Lvalue reference[3],

Rvalue reference to Rvalue reference[4]

等四種狀況,那麼針對這樣的狀況,編譯器將會根據引用摺疊規則變爲一個Single Reference,那麼是左值引用仍是右值引用呢?其實這個規則很簡單,只要有一個是左值引用,那麼結果就是左值引用,其他的就是右值引用。因而咱們知道了[1][2][3]的結果都是左值引用,只有[4]會是右值引用,而要從

1 void foo(T&& fparam)

這裏T的Universal Reference讓fparam擁有右值引用類型,那麼則須要保證傳遞歸來的參數爲右值才能夠,由於如果左值的話,T會deduce成左值引用,結合引用摺疊規則,fparam的類型會是左值引用類型。

因而咱們如今來看,int& &&這樣的狀況屬於Lvalue reference to Rvalue reference,結果則爲左值引用。那麼,咱們這個時候帶入到forward函數來看看,首先是T變爲了int&,通過了remove_reference變爲了int,結合後面跟上的&,則變爲了int&。而後咱們再次替換 static_cast和return type的T爲int&,都獲得了int& &&

1 int& && forward(int& param)
2 {
3     return static_cast<int& &&>(param);
4 }

因而再應用引用摺疊規則,int& &&都劃歸爲了int&

1 int& forward(int& param)
2 {
3     return static_cast<int&>(param);
4 }

因而,咱們能夠發現咱們fparam變量的左值引用類型被保留了下來。這裏也須要注意,咱們到達forward的時候就已是左值引用了,因此forward並無改變什麼。

如咱們這時候是47這樣的右值,咱們知道了T會被deduce成int,通過了remove_reference,變爲了int,跟上後面的&,成爲了int&,而後再次替換static_cast和返回類型的T爲int&&

1 int && forward(int& param)
2 {
3     return static_cast<int&&)(param);
4 }

因而,咱們也能夠發現,咱們fparam變量的右值引用類型也完美的保留了下來。

相關文章
相關標籤/搜索