C++11 學習筆記 右值引用

一.右值引用

C++11增加了一個新的類型,稱爲右值引用(R-value reference),標記爲T &&。右值是指表達式結束後就不再存在的臨時對象。相對應的左值就是指表達式結束後依然存在的持久對象,所有的具名變量或對象都是左值,而右值不具名。一個區分左值與右值的便捷方法是:看能不能對表達式取地址,如果能,則爲左值。

在C++11中,右值由兩個概念構成,一個是將亡值(xvalue,expiring value)(C++11新增的,與右值引用相關的表達式,比如將要被移動的對象,T&& 函數返回值,std::move返回值和轉換爲T&&的類型的轉換函數的返回值),另一個是純右值(rvalue, PureRvalue)(非引用返回的臨時變量,運算表達式產生的臨時言火日王,原始字面量和lambda表達式等都是純右值。C++11中所有的值必屬於左值,將亡值,純右值三者之一。


1.&&的特性

與左值引用相類似,右值引用就是對右值進行引用的類型。因爲右值不具名,所以,我們只能通過引用的方式找到它。

1).聲明右值引用時必須立即進行初始化。因爲引用類型並不擁有所綁定對象的別名,只是該對象的一個別名。

2).通過右值引用的聲明,該右值的生命週期將會與右值引用類型變量的生命週期一樣,只要變量還在,右值就會一直存活。


#include <iostream>

using namespace std;

int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;

struct A
{
     A(){
         cout<<"construct: "<<++g_constructCount<<endl;
     }

     A(const A& a){
         cout<<"copy construct: "<<++g_copyConstructCount<<endl;
     }

     ~A(){
         cout<<"destruct: "<<++g_destructCount<<endl;
    }
};

A GetA(){
     return A();
}

int main(){
     A a = GetA();
     return 0;
}
在關閉返回值優化的情況下,輸出結果:

construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3
拷貝構造函數調用了兩次:

1).GetA()函數內部創建的對象返回後構造一個臨時對象時調用。

2).在main函數中構造a對象時調用。


優化(右值引用綁定了右值,讓臨時右值的生命週期延長了):

int main(){
     A&& a = GetA();
     return 0;
}

//輸出結果:
construct: 1
copy construct: 1
destruct: 1
destruct: 2
在C++98/03中,通過常量左值引用也經常用來做性能優化,輸出結果與右值引用一樣。因爲常量左值引用是一個“萬能”的引用類型,可以接受左值,右值,常量左值和常量右值。


實際上,T&&並不是一定表示右值,它綁定的類型是未定的,即可能是左值又可能是右值。

template <typename T>
void f(T&& param);

f(10);                 //param是右值
int x = 10;
f(x);                   //param是左值


在像上面這樣的例子中,當發生自動類型推導時(如函數模板的類型自動推導,或auto關鍵字),&&纔是一個universal references(未定的引用類型),它是左值還是右值引用取決於它的初始化。 需要注意的是,有一個很關鍵的規則:universal references僅僅在T&&下發生,任何一點附加條件都會使之失效,而變成一個普通的右值引用。

template<typename T>
void f(T&& param);           //universal references

template<typename T>
class Test{
     ...
     Test(Test&& hrs);          //右值引用
     ...
};

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

template <typename T>
void f (std::vector<T>&& param);     //右值引用

temple <typename T>
void f(const T&& param);             //右值引用

記住,如果不是universal references,用一個左值初始化一個右值引用類型是不合法的。

正確的做法是使用std::move將一個左值轉換成右值。

int w1;
decltype(w1)&& v1 = w1;    //error

decltype(w1)&& v1 = std::move(w2);

編譯器會將己命名的右值引用視爲左值,而將未命名的右值引用視爲右值。

void PrintValue(int& i){
     std::cout<<"lvalue : "<<i<<std::endl;
}

void PrintValue(int&& i){
     std::cout<<"rvalue : "<<i<<std::endl;
}

void Forward(int&& i){
    PrintValue(i);
}

int main(){
    int i=0;
    PrintValue(i);
    PrintValue(1);
    Forward(2);
}

輸出結果:
lvalue : 0
rvalue : 1
lvalue : 2

2.右值引用優化性能,避免深拷貝(C++11加入右值引用的原因)

對於含有堆內存的類,我們都需要提供其深拷貝的構造函數,否則,會使用其默認提供的拷貝構造函數,容易導致堆內存的重複刪除,指針指向爲空。

class A
{
public:
     A():m_ptr(new int(0)){}
     ~A(){
         delete m_ptr;
     }
private:
     int* m_ptr;
};

A get(bool flag){
     A a;
     A b;
     if(flag){
          return a;
     }else{
          return b;
     }
}

int main(){
     A a = Get(false);  //臨時變量的m_ptr指向爲空,析構時,重複刪除引起錯誤...
}
下面是正確的做法,提供了深拷貝的拷貝構造函數

class A
{
public:
     A() : m_ptr(new int(0)){
          cout<<"constructor"<<endl;
     }
     A(const A& a):m_ptr(new int(*a.m_ptr)){
          cout<<"copy construct"<<endl;
     }

     ~A(){
          cout<<"destruct"<<endl;
          delete m_ptr;
     }

private:
     int* m_ptr;
};


int main(){
    A a = Get(false);
}

輸出結果:
construct
construct
copy construct
destruct
destruct
destruct

這樣雖然是安全的,但是卻因爲拷貝構造帶來了額外的損耗。

Get函數會返回臨時變量,然後通過臨時變量拷貝構造一個新的對象b,臨時變量在拷貝構造完成之後銷燬了,如果堆內存很大,那麼這個拷貝構造的代價會很大。因爲可以使用移動構造函數(對右值引用進行淺拷貝)。

class A
{
public:
     A():m_ptr(new int(0)){
          cout<<"construct"<<endl;
     }

     A(const A& a):m_ptr(new int(*a.m_ptr)){
          cout<<"copy construct"<<endl;
     }

     A(A&& a) : m_ptr(a.m_ptr){
           a.m_ptr = nullptr;
           cout<<"move construct: "<<endl;
     }

     ~A(){
           cout<<"destruct"<<endl;
           delete m_ptr;
     }

private:
     int* m_ptr;
};

int main(){
     A a = Get(false);
}

//輸出結果
construct
construct
move construct
destruct
destruct
destruct
移動構造函數中,它的參數是一個右值引用類型的參數A&&,這裏沒有深拷貝,只有淺拷貝,這樣就避免了對臨時對象的深拷貝,提高了性能。這裏的A&&用來根據參數是左值還是右值來建立分支,如果是臨時值,則會選擇移動構造函數。右值引用的一個重要的目的是用來支持移動語義的。


下面看一個MyString類實現的例子

class MyString{
private:
     char * m_data;
     size_t m_len;
     
     void copy_data(const char *s){
          m_data = new char[m_len+1]; 
          memcpy(m_data, s, m_len);
          m_data[m_len]='\0';
     }

public:
     MyString(){
          m_data = NULL;
          m_len = 0;
     }

     MyString(const char* p ){
          m_len = strlen(p);
          copy_data(p);
     }

     MyString(const MyString& str){
          m_len = str.m_len;
          copy_data(str.m_data);
          std::cout<<"Copy Constructor is called! source: "<<str.m_data<<std::endl;
}

     MyString& operator=(const MyString& str){
           if(this!=&str){
                m_len = str.m_len;
                copy_data(str._data);
           }
           std::cout<<"Copy Assignment is called! source: "<<str.m_data<<std::endl;
           return *this;
     }

     virtual ~MyString(){
           if(m_data)
                 delete[] m_data;
     }
};

int main(){
      MyString a;
      a = MyString("Hello");
      std::vector<MyString> vec;
      vec.push_back(MyString("World"));
      return 0;
}

//MyString的移動構造函數和移動賦值函數
MyString(MyString&& str){
      std::cout<<"Move Constructor is called! source: "<<str._data<<std::endl;
      _len=str._len;
      _data=str._data;
      str._len=0;
      str._data=NULL;
}

MyString& operator=(MyString&& str){
     std::cout<<"Move Assignment is called! source: "<<str._data<<std::endl;
     if(this!=&str){
          _len=str._len;
          _data=str._data;
          str._len=0;
          str._data=NULL;
     }
     return *this;
}

3.move語義

移動語義是通過右值引用來匹配臨時值的,普通的左值該怎麼辦呢?C++11提供了std::move方法來將左值轉換爲右值,從而方便應用移動語義。move是將對象的內存或者所有權從一個對象轉移到另一個對象,只是轉移,沒有內存拷貝,將一個左值強制轉換爲一個右值引用。

std::list<std::string> tokens;
std::list<std::string> t = std::move(tokens);

使用了move幾乎沒有任何代價,只是轉換了資源的所有權。實際上是將左值變成右值引用,然後應用move語義調用構造函數,就避免了拷貝,提高了性能。


4.forward和完美轉發

一個右值引用參數作爲函數的形參,在函數內部再轉發該參數時變成了一個左值,不是原來的類型了。

template <typename T>
void forwardValue(T& val){
     processValue(val);                   //右值參數會變成左值
}

template <typename T>
void forwardValue(const T& val){
     processValue(val);                 //參數都變成常量左值引用了
}
因此,我們需要一種方法能按照參數原來的類型轉發到另一個函數,這種轉發稱爲完美轉發。C++11提供了一個函數std::forward,爲轉發而生,不管參數是T&&這種未定的引用還是明確的左值引用或者右值引用,都會按照參數本來的類型轉發。

void Print(int& t){
     cout<<"lvalue"<<endl;
}

template <typename T>
void PrintT(int &t){
     cout<<"rvalue"<<endl;
}

template <typename T>
void TestForward(t && v){
     PrintT(v);
     PrintT(std::forward<T>(v));
     PrintT(std::move(v));
}

Test(){
     TestForward(1);
     int x=1;
     TestForward(x);
     TestForward(std::forward<int>(x));
}

//輸出結果:
lvalue
rvalue
rvalue

5.emplace_back減少內存拷貝和移動

emplace_back能就地通過參數構造對象,不需要拷貝或者移動內存,相比push_back能更好地避免內存的拷貝和移動,使容器查入元素的性能得到進一步提升。在大多數情況下應該優先使用emplace_back來代替push_back。所有的標準庫容器(array除外,因爲它長度不可以變,不能插入元素)都增加了類似的方法:emplace,emplace_hint,emplace_front,emplace_after和emplace_back。

#include <vector>
#include <iostream>

using namespace std;

struct A
{
     int x;
     double y;
     A(int a, double b):x(a),y(b){}
};

int main(){
     vector<A> v;
     v.emplace_back(1,2);
     cout<<v.size()<<endl;
     return 0;
}
emplace_back的用法比較簡單,直接通過構造函數的參數就可以構造對象,因此,也要求對象必須有對應的構造函數。如果沒有,編譯器會報錯。


#include <vector>
#include <map>
#include <string>
#include <iostream>

using namespace std;

struct Complicated
{
     int year;
     double country;
     std::string name;

     Complicated(int a, double b, string c):year(a),country(b),name(c){
          cout<<"is constucted"<<endl;
     }

     Complicated(const Complicated& other):year(other.year),country(other.country),name(std::move(other.name)){
          cout<<"is moved"<<endl;
     }
};

int main(){
     std::map<int, Complicated> m;
     int anInt = 4;
     double aDouble = 5.0;
     std::string aString = "C++"
     cout<<"--insert--"<<endl;
     m.insert(std::make_pair(4,Complicated(anInt, aDouble, aString)));

     cout<<"--emplace--"<<endl;
     m.emplace(4, Complicated(anInt, aDouble, aString));
     
     cout<<"--emplace_back--"<<endl;
     vector<Complicated> v;
     v.emplace_back(anInt, aDouble, aString);
     cout<<"--push_back--"<<endl;
     v.push_back(Complicated(anInt, aDouble, aString));

     return 0;
}

//輸出結果:
--insert--
is constructed
is moved
is moved
--emplace--
is constructed
is moved
--emplace_back--
is constructed
--push_back--
is constructed
is moved
is moved










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章