本文翻譯自:https://docs.microsoft.com/en-us/cpp/cpp/references-cpp?view=vs-2019,並參考《深入理解C++11》。
引用,類似於指針,用於存儲一個位於內存某處的對象的地址。與指針不同的是,引用在被初始化后不能再指向另一個對象,或設置為null。引用分為兩種:左值引用,右值引用,其中左值引用指向一個命名的變量,右值引用指向一個臨時對象(temporary object)。操作符&表示左值引用,而&&根據其上下文的不同可表示右值引用或a universal reference。
注:universal reference:https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
在C++11中,右值引用指的是對一個右值進行引用的類型,為了區別於C++98中的引用類型,稱C++98中的引用為“左值引用(lvalue reference)”。右值引用和左值引用都是引用類型,引用類型必須在聲明的時候立即初始化,其原因可以理解為引用類型本身並不擁有所綁定的對象的內存,只是該對象的一個別名。左值引用時具名變量值得別名,而右值引用是不具名(匿名)變量的別名。
左值引用
作用:持有一個對象的地址,但是其行為類似於一個對象
格式:type-id & cast-expression
我們可以將左值引用看為一個對象的別名。左值引用的聲明包含一個可選的說明符列表,后面跟一個引用聲明符。左值引用必須被初始化,且不能再指向另外一個對象或設置為null。
任何可以將其地址轉換為一個指定類型指針的對象也可將其地址轉換為一個類似的引用對象。例如,任何可以轉換為char*的對象的地址也可以轉換為char &。
注意不要將引用聲明符(&)與地址操作符(取對象的地址,也是&)混淆。當&前面是一個類型時,例如int或char,則該&是一個引用聲明符。如果&前面沒有任何類型,則該&用於取一個對象的地址。
- 例子:
The following example demonstrates the reference declarator by declaring a Person object and a reference to that object. Because rFriend is a reference to myFriend, updating either variable changes the same object.
// reference_declarator.cpp // compile with: /EHsc // Demonstrates the reference declarator. #include <iostream> using namespace std; struct Person { char* Name; short Age; }; int main() { // Declare a Person object. Person myFriend; // Declare a reference to the Person object. Person& rFriend = myFriend; // Set the fields of the Person object. // Updating either variable changes the same object. myFriend.Name = "Bill"; rFriend.Age = 40; // Print the fields of the Person object to the console. cout << rFriend.Name << " is " << myFriend.Age << endl; }
輸出:
Bill is 40
右值引用
右值引用指向臨時對象。在C++11之前,可以通過一個左值引用指向一個臨時對象,但是這個左值引用必須是const:
string getName() { return “Alex”; } const string& name = getName();
這也表明,臨時對象並不是立即被銷毀(destructed),這由C++保證,但它仍然是一個臨時對象,你不能修改它的值。
在C++11中,引入了右值引入,可以通過一個可變引用指向rvalue,但是不能綁定到lvalue,因此右值引用可以檢測一個值是否是臨時對象。
類似於左值引用使用&,右值引用使用&&,可以是const/non-const
持有一個指向右值表達式的引用。
- 格式:type-id && cast-expression
右值引用可以用於區分一個表達式是左值還是右值。左值引用與右值引用在句法上語法上類似,但是遵從不同的規則。下面章節用於描述右值引用是如何支持移動語義(move semantics)和完美轉發(perfect forwarding)的實現的。
左值引用綁定到右值
通常情況下右值引用不能綁定到任何的左值,例如下面代碼無法通過編譯:
int c; int && d = c;
但是,C++98標准中就出現的左值引用是否可以綁定到右值(由右值進行初始化)?
T& e = ReturnRvalue(); const T& f = ReturnRvalue();
上述代碼中,e的初始化會導致編譯錯誤,而f則可以通過編譯。C++98中的左值引用可以接受:非常量左值、常量左值。右值對其進行初始化。而使用右值對其進行初始化時,常量左值引用還可以像右值引用一樣延長右值的聲明周期,但是必須是const。
看如下代碼有何區別?
const bool& judgement = true; const bool judgement = true;
第一個使用常量左值引用來綁定右值,該語句執行后右值true並沒有被銷毀,而第二個表達式在使用右值true構造judgement后,右值true就會被銷毀了。

判斷類型
<type_traits>頭文件提供了3個模板類:
- is_rvalue_reference
- is_lvalue_reference
- is_reference
<type_traits> This header defines a series of classes to obtain type information on compile-time.該頭文件定義了一系列的類,用於獲取編譯期對象的類型信息。
Move Semantics移動語義
移動語義(move semantics)的實現依賴於右值引用,move可以明顯地提升應用的性能。移動語義讓你可以通過代碼將資源(例如動態分配的內存)從一個對象轉移到另一個對象。移動語義可以工作的原理是:它可以將資源從臨時對象(temporary objects)中轉移到其他地方,這些臨時變量在程序的其他地方不會被獲取到(be referenced)。
為了實現移動語義,你需要給自定義的類提供:移動構造函數(move constructor),另外可選地提供移動賦值構造符(move assignment operator, operator=)。(如果實現了這些函數),持有右值資源的對象的拷貝和賦值操作符會自動使用move semantics。與默認的拷貝構造函數不同的是,編譯器並不會提供一個默認的移動構造函數。移動構造函數:https://docs.microsoft.com/en-us/cpp/cpp/move-constructors-and-move-assignment-operators-cpp?view=vs-2019
你也可以重載普通函數和操作符來使用move semantics。Visual Studio 2010將move semantics引入到C++標准庫中。例如:string類實現了使用move semantics的相關操作。例如:
// string_concatenation.cpp // compile with: /EHsc #include <iostream> #include <string> using namespace std; int main() { string s = string("h") + "e" + "ll" + "o"; cout << s << endl; }
在Visual Studio 2010之前,string的每一個+操作符都會分配並返回一個新的臨時的string對象(an rvalue)。+操作符並不能將一個string擴展到另一個string上,因為它不知道the source string是左值還是右值。如果+左右的兩個字符串都是左值,它們可能在程序的其他地方被引用,因此不能修改。通過使用右值引用,可以修改+操作符用於右值,因為右值不會在程序的其他地方被引用。這就可以明顯地降低string類必須的動態內存分配。
當編譯器不能使用Reture Value Optimization(RVO,返回值優化),或Named Return Value Optimization(NRVO)時,移動語義也能提升程序性能。在這種情況下,如果返回類型定義了move constructor的話編譯器會調用它。
為了更好地理解移動語義,考慮向vector對象中插入元素這樣的一個例子。如果vector對象的容量超出了,vector對象為其存儲的元素需要重定位內存,然后將每個元素拷貝到另一個內存地址,以為新插入的元素騰出空間。當插入操作拷貝一個元素時,他會創建一個新的元素,調用拷貝構造函數以將之前的數據拷貝到新元素中,然后再將之前的元素析構掉(destroy)。而移動語義讓你可以直接地將元素進行移動,而不需要進行昂貴的內存重定位,和復制操作。為了使用move semantics,你可以寫一個move構造函數,用於將數據從一個對象移動到另一個。
在C++11中,標准庫<utility>中提供了函數:std::move,該函數功能為:將一個左值強制轉化為右值引用,繼而我們可以通過右值引用使用該值,以用於移動語義。從實現上講,std::move基本等同於一個類型轉換:
static_cast<T&&>(lvalue);
移動語義的一個典型的應用是實現高性能的置換(swap)函數。
template<class T> void swap(T& a, T& b) { T temp(move(a)); a = move(b); b = move(temp); }
如果上述代碼中的類型T是可以移動的(有移動構造函數,移動賦值函數),整個過程中代碼都只會按照移動語義進行指針交換,不會有資源的釋放與申請。如果T是不可移動的卻是可拷貝的,則拷貝語義會被用來進行置換,這就與普通的置換語句相同了。
注意:如果在移動構造函數中拋出異常是很危險的,因為會導致空懸的指針,因此最好給移動構造函數添加一個noexcept關鍵字,如果移動構造函數拋出異常則直接調用teminate函數終止程序,而不是造成指針空懸的狀態。
在<utility>頭文件中定義了:move_if_noexcept,
Move if noexcept:
Returns an rvalue reference to arg, unless copying is a better option than moving to provide at least a strong exception guarantee.該函數在類的移動構造函數沒有noexcept關鍵字修飾時返回一個左值引用從而使變量可以使用拷貝語義,而在類的移動構造函數有noexcept關鍵字時,返回一個右值引用,從而使變量可以使用移動語義。
#include <utility> #ifdef _NOEXCEPT #define noexcept _NOEXCEPT #endif // _NOEXCEPT struct Maythrow { Maythrow(){} Maythrow(const Maythrow&) { cout << "Maythrow copy constructor" << endl; } Maythrow(Maythrow&&) { cout << "Maythrow move constructor" << endl; } }; struct Nothrow { Nothrow(){} Nothrow(Nothrow&&) noexcept { cout << "Nothrow move constructor" << endl; } Nothrow(const Nothrow&) { cout << "Nothrow copy constructor" << endl; } }; int main(int argc, char *argv[]) { Maythrow m; Nothrow n; Maythrow mt = move_if_noexcept(m); Nothrow nt = move_if_noexcept(n);
}
輸出:

move_if_noexcpet是一種犧牲性能以保證安全的做法,且要求開發者對移動構造函數使用noexcept進行描述,否則就會損失性能(使用拷貝構造函數,見上面例子)。
注意:
在gcc4.8.5中編譯帶noexcept的構造函數時,編譯正常,但是在Windows環境下使用Visual Studio 2013編譯,會報錯:error C3646:"noexcept":未知重寫說明符,網上查了下資料,需要在代碼中添加預處理指令:然后就可以編譯成功了,好像是VS2015才支持noexcpet,對於__func__也是到VS2015才完全支持的,見:https://blog.csdn.net/weixin_43956273/article/details/100169367
#ifdef _NOEXCEPT
#define noexcept _NOEXCEPT
#endif // _NOEXCEPT
Perfect Fowarding完美轉發
https://blog.csdn.net/u012198575/article/details/83142419
https://docs.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=vs-2019
C++11中的一項新技術,指在函數模板中,完全依照模板的參數的類型,將參數傳遞給函數模板中調用的另外一個函數,例如:
template<typename T> void IamForwarding() { IrunCodeActually(t); }
上述例子中,IamForwarding是一個轉發函數模板,而函數IrunCodeActually是真正執行代碼的目標函數。上述代碼中只是轉發(forwarding)參數,在IrunCodeActaully調用之前就已經進行了一次臨時對象拷貝。如果要完美地轉發參數,需要使用引用類型,但問題是:目標函數可能接受左值引用,也可能接受右值引用。
C++11通過引入一條“引用折疊(reference cllapsing)”的語言規則,並結合新的模板推導規則來實現完美轉發。
Perfect forwarding用於降低重載函數的需求,並且當你編寫一個參數為引用的泛型函數時,且該泛型函數將參數傳遞(或:forward)給其他函數時,有助於解決forwarding problem。例如:如果泛型函數的參數類型為const T&,那么對該函數的調用不能修改參數的值。如果泛型函數的參數類型為T&,那么不同使用rvalue(例如:臨時對象或整數字面常數:temporary object or integer literal)對該函數進行調用。
對於該類問題的通常解決方法是對該泛型函數進行重載,對於每個參數都重載T& 和const T&的版本。但是,如果函數參數較多的話重載版本會指數型增加。
右值引用rvalue reference可以讓你通過一個版本的函數來接收這兩種函數,且可通過forward將這些參數傳遞給其他函數。
例如:對於4中類型:W X Y Z,它們的構造函數分別使用不同的const non-const左值引用組合:
struct W { W(int&, int&) {} }; struct X { X(const int&, int&) {} }; struct Y { Y(int&, const int&) {} }; struct Z { Z(const int&, const int&) {} };
假如你要編寫一個泛型函數來生成這些對象,可以寫成下面形式:
template <typename T, typename A1, typename A2> T* factory(A1& a1, A2& a2) { return new T(a1, a2); }
通過如下形式可以調用該泛型函數:
int a = 4, b = 5; W* pw = factory<W>(a, b);
但是通過如下形式調用會產生錯誤,因為參數是rvalue,而泛型函數接受的參數是可修改的左值引用(lvalue references that are modifiable)
Z* pz = factory<Z>(2, 2);
通常解決這類問題的方法是重載:每個參數都有一個A& ,const A&的變化。右值引用則可以讓你只寫一個版本的函數:
template <typename T, typename A1, typename A2> T* factory(A1&& a1, A2&& a2) { return new T(std::forward<A1>(a1), std::forward<A2>(a2)); }
這個版本的泛型函數使用右值引用rvalue reference作為factory函數的參數。std::forward函數的作用是forward the parameters of the factory to the constructor of the template class,是將factory函數的參數轉發給模板類的構造函數。
該版本可以支持如下的調用方式:
int main() { int a = 4, b = 5; W* pw = factory<W>(a, b); X* px = factory<X>(2, b); Y* py = factory<Y>(a, 2); Z* pz = factory<Z>(2, 2); delete pw; delete px; delete py; delete pz; }
Additional Properties of Rvalue Reference右值引用的其他特性
可以重載一個函數,讓它分別接受左值引用和右值引用。
通過重載一個函數,讓它分別接受const左值引用和右值引用,你可以通過代碼來判斷一個表達式是non-modifiable objects(lvalues)還是modifiable tempoary values(rvalues)。只有當一個對象被標記為const,你才能將它傳遞給一個參數為右值引用的函數。The following example shows the function f, which is overloaded to take an lvalue reference and an rvalue reference. The main function calls f with both lvalues and an rvalue.
// reference-overload.cpp // Compile with: /EHsc #include <iostream> using namespace std; // A class that contains a memory resource. class MemoryBlock { // TODO: Add resources for the class here. }; void f(const MemoryBlock&) { cout << "In f(const MemoryBlock&). This version cannot modify the parameter." << endl; } void f(MemoryBlock&&) { cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl; } int main() { MemoryBlock block; f(block); f(MemoryBlock()); }
該例子輸出結果如下:
In f(const MemoryBlock&). This version cannot modify the parameter. In f(MemoryBlock&&). This version can modify the parameter.
在該例子中,第一個調用f函數傳遞了一個局部變量(右值)作為參數。第二個調用f函數傳遞了一個臨時對象作為參數。由於臨時對象不能在程序的其他地方引用,因此調用重載函數中參數為右值的版本,且該版本中過對於傳入參數是可以修改的。
注意:編譯器將一個命名的右值引用視為左值進行處理,並將一個未命名的右值引用視為右值進行處理。The compiler treats a named rvalue reference as an lvalue and an unnamed rvalue reference as an rvalue.
當你編寫一個參數為右值引用的函數時,在函數體內該參數被視為是左值。編譯器將命名的右值引用當做一個左值進行處理,這是因為一個明明的對象可以在程序的多處被引用;但是允許程序的多處來修改或刪除資源時很危險的。例如:如果程序的多個位置都嘗試從一個對象中轉移資源,僅僅會有一個對象能成功地轉移該資源(source)。
下面例子定義了一個函數g,該函數重載了一個左值引用和右值引用作為參數。函數f的參數為左值引用(一個明明的右值引用),並返回一個右值引用(一個未命名的右值引用)。在f函數中調用g,overload resolution選擇了參數為左值引用版本的函數g,因為f的函數體將其參數視為左值。在main函數中調用g,overload resolution選擇了參數為優質引用版本的函數g,因為f函數返回的是一個右值引用。
// named-reference.cpp // Compile with: /EHsc #include <iostream> using namespace std; // A class that contains a memory resource. class MemoryBlock { // TODO: Add resources for the class here. }; void g(const MemoryBlock&) { cout << "In g(const MemoryBlock&)." << endl; } void g(MemoryBlock&&) { cout << "In g(MemoryBlock&&)." << endl; } MemoryBlock&& f(MemoryBlock&& block) { g(block); return move(block); } int main() { g(f(MemoryBlock())); }
上面例子的輸出為:
In g(const MemoryBlock&). In g(MemoryBlock&&).
在該例子中,mian函數傳遞一個右值給f函數。f函數體將它的參數當做是左值進行處理,函數f調用g時調用的是g的左值引用的版本,因此第一個打印的是左值引用版本中的內容。
- 你可以將一個左值轉換為右值引用
C++標准庫函數std::move允許你將一個對象轉換為該對象的右值引用。另外,你可以使用static_cast關鍵字將一個左值轉換為右值引用,看下面的例子:
// cast-reference.cpp // Compile with: /EHsc #include <iostream> using namespace std; // A class that contains a memory resource. class MemoryBlock { // TODO: Add resources for the class here. }; void g(const MemoryBlock&) { cout << "In g(const MemoryBlock&)." << endl; } void g(MemoryBlock&&) { cout << "In g(MemoryBlock&&)." << endl; } int main() { MemoryBlock block; g(block); g(static_cast<MemoryBlock&&>(block)); }
輸出:
In g(const MemoryBlock&). In g(MemoryBlock&&).
- 函數模板推斷它們的模板參數類型,然后使用引用折疊規則。Function template deduce their template argument types and then use reference collapsing rules.
編寫函數模板然后將其參數再傳遞給其他函數的行為很常見。理解參數為右值引用的函數模板的模板類型推斷(template type deduction)的工作原因很重要。
如果函數參數是一個右值,編譯器會推斷該參數為一個右值引用。例如,如果你傳遞一個類型為x的右值引用給一個參數為T&&的模板函數,模板參數推斷(template argument deduction)會將T推斷為X。因此,參數類型為X&&。如果函數參數是一個左值或常量左值(const lvalue),則編譯器會將類型推斷為該類型的左值引用或const左值引用。
下面例子中聲明了一個結構體模板,然后將它實例化為各個類型。print_type_and_value函數的參數為右值引用,然后forward該參數給合適的實例化版本的S::print函數。main函數展示了調用S::print函數的多種方式。
// template-type-deduction.cpp // Compile with: /EHsc #include <iostream> #include <string> using namespace std; template<typename T> struct S; // The following structures specialize S by // lvalue reference (T&), const lvalue reference (const T&), // rvalue reference (T&&), and const rvalue reference (const T&&). // Each structure provides a print method that prints the type of // the structure and its parameter. template<typename T> struct S<T&> { static void print(T& t) { cout << "print<T&>: " << t << endl; } }; template<typename T> struct S<const T&> { static void print(const T& t) { cout << "print<const T&>: " << t << endl; } }; template<typename T> struct S<T&&> { static void print(T&& t) { cout << "print<T&&>: " << t << endl; } }; template<typename T> struct S<const T&&> { static void print(const T&& t) { cout << "print<const T&&>: " << t << endl; } }; // This function forwards its parameter to a specialized // version of the S type. template <typename T> void print_type_and_value(T&& t) { S<T&&>::print(std::forward<T>(t)); } // This function returns the constant string "fourth". const string fourth() { return string("fourth"); } int main() { // The following call resolves to: // print_type_and_value<string&>(string& && t) // Which collapses to: // print_type_and_value<string&>(string& t) string s1("first"); print_type_and_value(s1); // The following call resolves to: // print_type_and_value<const string&>(const string& && t) // Which collapses to: // print_type_and_value<const string&>(const string& t) const string s2("second"); print_type_and_value(s2); // The following call resolves to: // print_type_and_value<string&&>(string&& t) print_type_and_value(string("third")); // The following call resolves to: // print_type_and_value<const string&&>(const string&& t) print_type_and_value(fourth()); }
上述例子輸出為:
print<T&>: first print<const T&>: second print<T&&>: third print<const T&&>: fourth
為解析每次對函數print_type_and_value函數的調用,編譯器首先進行模板變量推斷。當編譯器將推導的模板參數替換為參數類型時,編譯器運用引用折疊規則(reference collapsing rule)。例如,將局部變量s1傳遞給print_type_and_value函數時,編譯器會產生如下的函數簽名:
print_type_and_value<string&>(string& && t)
編譯器使用引用折疊規則將簽名減少到以下內容:
print_type_and_value<string&>(string& t)
這個版本的print_type_and_value函數然后forward(轉發)它的參數到正確版本的S::print函數。
下面表格總結了模板變量類型推斷的引用折疊規則:
| Expanded type | Collapsed type |
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |

模板變量類型推斷是實現perfect forwarding(完美轉發)的重要組成部分。
總結
右值引用可以用於區分左值,右值。右值引用可以通過消除不必要的內存分配和copy行為來提升你的程序的性能。右值引用還可以讓你只寫一個版本的函數,就可以接受不同的參數並forward轉發該參數到其他函數,就像其他函數被直接調用一樣。They also enable you to write one version of a function that accepts arbitrary arguments and forwards them to another function as if the other function had been called directly.
