右值引用 (Rvalue Referene) 是 C++ 新标準 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它實現了轉移語義 (Move Sementics) 和精确傳遞 (Perfect Forwarding)。它的主要目的有兩(liǎng)個方面(miàn):
消除兩(liǎng)個對(duì)象交互時不必要的對(duì)象拷貝,節省運算存儲資源,提高效率。
能(néng)夠更簡潔明确地定義泛型函數。
C++( 包括 C) 中所有的表達式和變量要麼(me)是左值,要麼(me)是右值。通俗的左值的定義就是非臨時對(duì)象,那些可以在多條語句中使用的對(duì)象。所有的變量都(dōu)滿足這(zhè)個定義,在多條代碼中都(dōu)可以使用,都(dōu)是左值。右值是指臨時的對(duì)象,它們隻在當前的語句中有效。請看下列示例 :
如:int i = 0;
在這(zhè)條語句中,i 是左值,0 是臨時值,就是右值。在下面(miàn)的代碼中,i 可以被(bèi)引用,0 就不可以了。立即數都(dōu)是右值。
右值也可以出現在賦值表達式的左邊,但是不能(néng)作爲賦值的對(duì)象,因爲右值隻在當前語句有效,賦值沒(méi)有意義。
如:((i>0) ? i : j) = 1;
在這(zhè)個例子中,0 作爲右值出現在了”=”的左邊。但是賦值對(duì)象是 i 或者 j,都(dōu)是左值。
在 C++11 之前,右值是不能(néng)被(bèi)引用的,最大限度就是用常量引用綁定一個右值,如 :
const int &a = 1;
在這(zhè)種(zhǒng)情況下,右值不能(néng)被(bèi)修改的。但是實際上右值是可以被(bèi)修改的,如 :
T().set().get();
T 是一個類,set 是一個函數爲 T 中的一個變量賦值,get 用來取出這(zhè)個變量的值。在這(zhè)句中,T() 生成(chéng)一個臨時對(duì)象,就是右值,set() 修改了變量的值,也就修改了這(zhè)個右值。
既然右值可以被(bèi)修改,那麼(me)就可以實現右值引用。右值引用能(néng)夠方便地解決實際工程中的問題,實現非常有吸引力的解決方案。
左值的聲明符号爲”&”, 爲了和左值區分,右值的聲明符号爲”&&”。
示例程序 :
void process_value(int& i) {
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i) {
std::cout << "RValue processed: " << i << std::endl;
}
int main() {
int a = 0;
process_value(a);
process_value(1);
}
運行結果 :
LValue processed: 0
RValue processed: 1
Process_value 函數被(bèi)重載,分别接受左值和右值。由輸出結果可以看出,臨時對(duì)象是作爲右值處理的。
但是如果臨時對(duì)象通過(guò)一個接受右值的函數傳遞給另一個函數時,就會(huì)變成(chéng)左值,因爲這(zhè)個臨時對(duì)象在傳遞過(guò)程中,變成(chéng)了命名對(duì)象。
示例程序 :
void process_value(int& i) {
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i) {
std::cout << "RValue processed: " << i << std::endl;
}
void forward_value(int&& i) {
process_value(i);
}
int main() {
int a = 0;
process_value(a);
process_value(1);
forward_value(2);
}
運行結果 :
LValue processed: 0
RValue processed: 1
LValue processed: 2
雖然 2 這(zhè)個立即數在函數 forward_value 接收時是右值,但到了 process_value 接收時,變成(chéng)了左值。
右值引用是用來支持轉移語義的。轉移語義可以將(jiāng)資源 ( 堆,系統對(duì)象等 ) 從一個對(duì)象轉移到另一個對(duì)象,這(zhè)樣(yàng)能(néng)夠減少不必要的臨時對(duì)象的創建、拷貝以及銷毀,能(néng)夠大幅度提高 C++ 應用程序的性能(néng)。臨時對(duì)象的維護 ( 創建和銷毀 ) 對(duì)性能(néng)有嚴重影響。
轉移語義是和拷貝語義相對(duì)的,可以類比文件的剪切與拷貝,當我們將(jiāng)文件從一個目錄拷貝到另一個目錄時,速度比剪切慢很多。
通過(guò)轉移語義,臨時對(duì)象中的資源能(néng)夠轉移其它的對(duì)象裡(lǐ)。
在現有的 C++ 機制中,我們可以定義拷貝構造函數和賦值函數。要實現轉移語義,需要定義轉移構造函數,還(hái)可以定義轉移賦值操作符。對(duì)于右值的拷貝和賦值會(huì)調用轉移構造函數和轉移賦值操作符。如果轉移構造函數和轉移拷貝操作符沒(méi)有定義,那麼(me)就遵循現有的機制,拷貝構造函數和賦值操作符會(huì)被(bèi)調用。
普通的函數和操作符也可以利用右值引用操作符實現轉移語義。
以一個簡單的 string 類爲示例,實現拷貝構造函數和拷貝賦值操作符。
示例程序 :
class MyString {
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
MyString() {
_data = NULL;
_len = 0;
}
MyString(const char* p) {
_len = strlen (p);
_init_data(p);
}
MyString(const MyString& str) {
_len = str._len;
_init_data(str._data);
std::cout << "Copy Constructor is called! source: " << str._data << std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
_len = str._len;
_init_data(str._data);
}
std::cout << "Copy Assignment is called! source: " << str._data << std::endl;
return *this;
}
virtual ~MyString() {
if (_data) free(_data);
}
};
int main() {
MyString a;
a = MyString("Hello");
std::vector<MyString> vec;
vec.push_back(MyString("World"));
}
運行結果 :
Copy Assignment is called! source: Hello
Copy Constructor is called! source: World
這(zhè)個 string 類已經(jīng)基本滿足我們演示的需要。在 main 函數中,實現了調用拷貝構造函數的操作和拷貝賦值操作符的操作。MyString(“Hello”) 和 MyString(“World”) 都(dōu)是臨時對(duì)象,也就是右值。雖然它們是臨時的,但程序仍然調用了拷貝構造和拷貝賦值,造成(chéng)了沒(méi)有意義的資源申請和釋放的操作。如果能(néng)夠直接使用臨時對(duì)象已經(jīng)申請的資源,既能(néng)節省資源,有能(néng)節省資源申請和釋放的時間。這(zhè)正是定義轉移語義的目的。
我們先定義轉移構造函數。
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;
}
和拷貝構造函數類似,有幾點需要注意:
參數(右值)的符号必須是右值引用符号,即“&&”。
參數(右值)不可以是常量,因爲我們需要修改右值。
參數(右值)的資源鏈接和标記必須修改。否則,右值的析構函數就會(huì)釋放資源。轉移到新對(duì)象的資源也就無效了。
現在我們定義轉移賦值操作符。
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;
}
這(zhè)裡(lǐ)需要注意的問題和轉移構造函數是一樣(yàng)的。
增加了轉移構造函數和轉移複制操作符後(hòu),我們的程序運行結果爲 :
Move Assignment is called! source: Hello
Move Constructor is called! source: World
由此看出,編譯器區分了左值和右值,對(duì)右值調用了轉移構造函數和轉移賦值操作符。節省了資源,提高了程序運行的效率。
有了右值引用和轉移語義,我們在設計和實現類時,對(duì)于需要動态申請大量資源的類,應該設計轉移構造函數和轉移賦值函數,以提高應用程序的效率。
既然編譯器隻對(duì)右值引用才能(néng)調用轉移構造函數和轉移賦值函數,而所有命名對(duì)象都(dōu)隻能(néng)是左值引用,如果已知一個命名對(duì)象不再被(bèi)使用而想對(duì)它調用轉移構造函數和轉移賦值函數,也就是把一個左值引用當做右值引用來使用,怎麼(me)做呢?标準庫提供了函數 std::move,這(zhè)個函數以非常簡單的方式將(jiāng)左值引用轉換爲右值引用。
示例程序 :
void ProcessValue(int& i) {
std::cout << "LValue processed: " << i << std::endl;
}
void ProcessValue(int&& i) {
std::cout << "RValue processed: " << i << std::endl;
}
int main() {
int a = 0;
ProcessValue(a);
ProcessValue(std::move(a));
}
運行結果 :
LValue processed: 0
RValue processed: 0
std::move在提高 swap 函數的的性能(néng)上非常有幫助,一般來說,swap函數的通用定義如下:
template <class T> 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 函數的定義變爲 :
template <class T> 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
}
通過(guò) std::move,一個簡單的 swap 函數就避免了 3 次不必要的拷貝操作。
本文采用精确傳遞表達這(zhè)個意思。”Perfect Forwarding”也被(bèi)翻譯成(chéng)完美轉發(fā),精準轉發(fā)等,說的都(dōu)是一個意思。
精确傳遞适用于這(zhè)樣(yàng)的場景:需要將(jiāng)一組參數原封不動的傳遞給另一個函數。
“原封不動”不僅僅是參數的值不變,在 C++ 中,除了參數值之外,還(hái)有一下兩(liǎng)組屬性:
左值/右值和 const/non-const。 精确傳遞就是在參數傳遞過(guò)程中,所有這(zhè)些屬性和參數值都(dōu)不能(néng)改變。在泛型函數中,這(zhè)樣(yàng)的需求非常普遍。
下面(miàn)舉例說明。函數 forward_value 是一個泛型函數,它將(jiāng)一個參數傳遞給另一個函數 process_value。
forward_value 的定義爲:
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 爲每一個參數必須重載兩(liǎng)種(zhǒng)類型,T& 和 const T&,否則,下面(miàn)四種(zhǒng)不同類型參數的調用中就不能(néng)同時滿足 :
int a = 0;
const int &b = 1;
forward_value(a); // int&
forward_value(b); // const int&
forward_value(2); // int&
對(duì)于一個參數就要重載兩(liǎng)次,也就是函數重載的次數和參數的個數是一個正比的關系。這(zhè)個函數的定義次數對(duì)于程序員來說,是非常低效的。我們看看右值引用如何幫助我們解決這(zhè)個問題 :
template <typename T> void forward_value(T&& val) {
process_value(val);
}
隻需要定義一次,接受一個右值引用的參數,就能(néng)夠將(jiāng)所有的參數類型原封不動的傳遞給目标函數。四種(zhǒng)不用類型參數的調用都(dōu)能(néng)滿足,參數的左右值屬性和 const/non-cosnt 屬性完全傳遞給目标函數 process_value。這(zhè)個解決方案不是簡潔優雅嗎?
int a = 0;
const int &b = 1;
forward_value(a); // int&
forward_value(b); // const int&
forward_value(2); // int&&
C++11 中定義的 T&& 的推導規則爲:
右值實參爲右值引用,左值實參仍然爲左值引用。
一句話,就是參數的屬性不變。這(zhè)樣(yàng)也就完美的實現了參數的完整傳遞。
右值引用,表面(miàn)上看隻是增加了一個引用符号,但它對(duì) C++ 軟件設計和類庫的設計有非常大的影響。它既能(néng)簡化代碼,又能(néng)提高程序運行效率。每一個 C++ 軟件設計師和程序員都(dōu)應該理解并能(néng)夠應用它。我們在設計類的時候如果有動态申請的資源,也應該設計轉移構造函數和轉移拷貝函數。在設計類庫時,還(hái)應該考慮 std::move 的使用場景并積極使用它。
右值引用和轉移語義是 C++ 新标準中的一個重要特性。每一個專業的 C++ 開(kāi)發(fā)人員都(dōu)應該掌握并應用到實際項目中。在有機會(huì)重構代碼時,也應該思考是否可以應用新也行。在使用之前,需要檢查一下編譯器的支持情況。
發(fā)表評論 取消回複