以下內容參考https://blog.csdn.net/china_jeffery/article/details/78520237
右值引用若不作為函數參數使用,基本等於濫用
右值引用 (Rvalue Referene) 是 C++ 新標准 中引入的新特性 , 它實現了移動語義 (Move Sementics) 和完美轉發 (Perfect Forwarding)。它的主要目的有兩個方面:
1. 消除兩個對象交互時不必要的對象拷貝,節省運算存儲資源,提高效率。
2. 能夠更簡潔明確地定義泛型函數。
何為右值:
C++( 包括 C) 中所有的表達式和變量要么是左值,要么是右值。通俗的左值的定義就是非臨時對象,
那些可以在多條語句中使用的對象。 所有的變量都滿足這個定義,在多條代碼中都可以使用,都是左值。
右值是指臨時的對象,它們只在當前的語句中有效。
int i = 0;// 在這條語句中,i 是左值,0 是臨時變量,就是右值
在C++11之前,右值是不能引的,如
int &a = 1;// error : 非常量的引用必須為左值 const int &a = 1;// 我們最多只能用常量引用來綁定一個右值
在C++11中我們可以引用右值,使用&&實現:
int &&a = 1;
應用場景:
如下string類,實現了拷貝構造和賦值運算符重載
#include <iostream> #include <vector> using namespace std; class MyString { public: MyString() { m_data = NULL; m_len = 0; } MyString(const char* s) { m_len = strlen(s); init_data(s); cout << "構造函數" << s << endl; } MyString(const MyString& str) { m_len = str.m_len; init_data(str.m_data); cout << "拷貝構造函數" << str.m_data << endl; } MyString& operator=(const MyString& str) { if ( this != &str ){ this->m_len = str.m_len; init_data(str.m_data); } cout << "等號操作符重載" << str.m_data << endl; return *this; } ~MyString() { if ( m_data != NULL ){ cout << "析構函數" << endl; free(m_data); } } private: void init_data(const char* s) { m_data = new char[m_len + 1]; memcpy(m_data, s, m_len); m_data[m_len] = '\0'; } char* m_data; size_t m_len; }; void test() { vector<MyString> vec; MyString a;// 沒有輸出 a = MyString("hello"); vec.push_back(MyString("world")); } int main() { test(); system("pause"); return 0; }
輸出:
構造函數hello
等號操作符重載hello
析構函數
構造函數world
拷貝構造函數world
析構函數
析構函數
析構函數
總共執行了2次拷貝,MyString("Hello")和MyString("World")都是臨時對象,臨時對象被使用完之后會被立即析構,在析構函數中free掉申請的內存資源。
如果能夠直接使用臨時對象已經申請的資源,並在其析構函數中取消對資源的釋放,這樣既能節省資源,有能節省資源申請和釋放的時間。 這正是定義移動語義的目的。
通過加入定義移動構造函數和轉移賦值操作符重載來實現右值引用(即復用臨時對象):
#include <iostream> #include <vector> using namespace std; class MyString { public: MyString() { m_data = NULL; m_len = 0; } MyString(const char* s) { m_len = strlen(s); init_data(s); cout << "構造函數" << s << endl; } MyString(MyString&& str) { cout << "移動構造函數" << str.m_data << endl; m_len = str.m_len; m_data = str.m_data; str.m_len = 0; str.m_data = NULL;// 防止在析構函數中將內存釋放掉 } MyString& operator=(MyString&& str) { cout << "移動等號操作符重載" << str.m_data << endl; if ( this != &str ){ this->m_len = str.m_len; this->m_data = str.m_data; str.m_len = 0; str.m_data = NULL;// 防止在析構函數中將內存釋放掉 } return *this; } ~MyString() { if ( m_data != NULL ){ cout << "析構函數" << endl; free(m_data); } } private: void init_data(const char* s) { m_data = new char[m_len + 1]; memcpy(m_data, s, m_len); m_data[m_len] = '\0'; } char* m_data; size_t m_len; }; void test() { vector<MyString> vec; MyString a;// 沒有輸出 a = MyString("hello"); vec.push_back(MyString("world")); } int main() { test(); system("pause"); return 0; }
輸出:
構造函數hello
移動等號操作符重載hello
構造函數world
移動拷貝構造函數world
析構函數
析構函數
需要注意的是:右值引用並不能阻止編譯器在臨時對象使用完之后將其釋放掉的事實,
所以移動構造函數和移動賦值操作符重載函數 中都將_data賦值為了NULL,而且析構函數中保證了_data != NULL才會釋放。
標准庫函數std::move
既然編譯器只對右值引用才能調用移動構造函數和移動賦值函數,又因為所有命名對象都只能是左值引用。
在這樣的條件了,如果已知一個命名對象不再被使用而想對它調用轉移構造函數和轉移賦值函數,
也就是把一個左值引用當做右值引用來使用,怎么做呢?
標准庫提供了函數 std::move,這個函數以非常簡單的方式將左值引用轉換為右值引用。
#include <iostream> using namespace std; void ProcessValue(int& i) { cout << "LValue processed:" << i << endl; } void ProcessValue(int&& i) { cout << "RValue processed:" << i << endl; } int main() { int a = 1; ProcessValue(a); ProcessValue(move(a)); system("pause"); return 0; }
輸出:
LValue processed:1
RValue processed:1
std::move在提高swap函數性能上有非常大的幫助,一般來說,swap函數的通用定義如下:
template <class T> void swap(T& a, T& b) { T tmp(a);// copy a to tmp a = b;// copy b to a b = tmp;//copy tmp to b }
結合std::move 和 右值引用,可以避免不必要的拷貝。swap的定義變為:
#include <iostream> #include <vector> using namespace std; class MyString { public: MyString() { m_data = NULL; m_len = 0; } MyString(const char* s) { m_len = strlen(s); init_data(s); cout << "構造函數" << s << endl; } MyString(MyString&& str) { cout << "移動構造函數" << str.m_data << endl; m_len = str.m_len; m_data = str.m_data; str.m_len = 0; str.m_data = NULL;// 防止在析構函數中將內存釋放掉 } MyString& operator=(MyString&& str) { cout << "移動等號操作符重載" << str.m_data << endl; if ( this != &str ){ this->m_len = str.m_len; this->m_data = str.m_data; str.m_len = 0; str.m_data = NULL;// 防止在析構函數中將內存釋放掉 } return *this; } ~MyString() { if ( m_data != NULL ){ cout << "析構函數" << endl; free(m_data); } } private: void init_data(const char* s) { m_data = new char[m_len + 1]; memcpy(m_data, s, m_len); m_data[m_len] = '\0'; } char* m_data; size_t m_len; }; namespace MyT{ template <class T> void swap(T& a, T& b) { T tmp(std::move(a));// move a to tmp a = std::move(b);// move b to a b = std::move(tmp);//move tmp to b } } void test() { MyString a("hello"); MyString b("world"); MyT::swap<MyString>(a, b); } int main() { test(); system("pause"); return 0; }
精確傳遞(perfect Forwarding)
精確傳遞就是在參數傳遞過程中,所有這些屬性和參數值都不能改變。在泛型函數中,這樣的需求非常普遍
forward_value函數只有一個參數val,定義如下:
template <typename T> void forward_value(const T& val) { process_value(val); } template <typename T> void forward_value(T& val) { process_value(val); }
函數 forward_value 為每一個參數必須重載兩種類型,T& 和 const T&,否則,下面四種不同類型參數的調用中就不能同時滿足:
int a = 0; const int &b = 1; forward_value(a); // int& forward_value(b); // const int& forward_value(2); // int&
對於一個參數就要重載兩次,也就是函數重載的次數和參數的個數是一個正比的關系。這個函數的定義次數對於程序員來說,是非常低效的。我們看看右值引用如何幫助我們解決這個問題:
template <typename T> void forward_value(T&& val) { process_value(val); }
只需要定義一次,接受一個右值引用的參數,就能夠將所有的參數類型原封不動的傳遞給目標函數。