右值引用

本頁使用了標題或全文手工轉換
維基百科,自由的百科全書

右值引用(rvalue reference),是C++程序設計語言C++11標準提出的一類數據類型。用於實現移動語義(move semantic)與完美轉發(perfect forwarding)。[參1]

背景[編輯]

在C++11提出右值引用之前,C++03及更早的C++標準中,表達式的「值分類」(value categories)屬性為左值或右值。[參2]左值是對應(refer to)內存中有確定存儲地址的對象之表達式的值,而右值是所有不是左值之表達式的值。因而,右值可以是字面量[註1]、臨時對象[註2]等表達式。能否被賦值不是區分C++左值與右值的依據,C++的const左值是不可賦值的;而作為臨時對象的右值可能允許被賦值[註3]。左值與右值的根本區別在於是否允許取址運算符(&)獲得對應的內存地址。[註4]C++03及以前的標準定義了在表達式中左值到右值的三類隱式自動轉換:

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

作為一種追求執行效率的語言,C++在用臨時對象或函數返回值給左值對象賦值時的深度拷貝(deep copy)一直受到詬病。考慮到臨時對象的生命期僅在表達式中持續,如果把臨時對象的內容直接移動(move)給被賦值的左值對象,效率改善將是顯著的。這就是移動語義的來源。

與傳統的拷貝賦值運算符(copy assignment)成員函數、拷貝構造(copy ctor)成員函數對應,移動語義需要有移動賦值(move assignment)成員函數、移動構造(move ctor)成員函數的實現機制。可以通過函數重載來確定是調用拷貝語義還是移動語義的實現。

右值引用就是為了實現移動語義與完美轉發所需要而設計出來的新的數據類型。右值引用的實例對應於臨時對象;右值引用並區別於左值引用,用作形參時能重載辨識(overload resolution)是調用拷貝語義還是移動語義的函數。

無論是傳統的左值引用還是C++11引進的右值引用,從編譯後的反匯編層面上,都是對象的存儲地址[註6]與自動解引用(dereference,取址的相反)。因此,右值引用與左值引用的變量都不能懸空(dangling),也即定義時必須初始化從而綁定到一個對象上。右值引用變量綁定的對象,是編程者認為可以通過移動語義移走其內容的對象,對這種對象就需要定義為一種獨特的值分類,即C++11標準稱之為「臨終值」(eXpire Value)。臨終值對象既有存儲地址因此可以綁定到右值引用變量上,而且它又是一個即將停止使用的對象可以被移走內容。所以臨終值既不同於左值,也不同於傳統的右值(C++11稱之為純右值),不能取地址運算(&)。另一方面,臨終值兼有傳統的左值與右值的性質:既對應於一個(臨時)對象,稱之為有標識(identity);同時其內容可以移走,稱之為可移動性(movability)。C++11標準把臨終值與左值合稱為廣義左值,即指向某個物理存在的對象;把臨終值與純右值(對應C++03時的右值概念)合稱為右值(C++11重新定義的概念),其內容可以移走(該右值生命期到此為止,此後將不再使用)。之所以稱為右值而不叫做廣義右值,是因為右值引用既可以與臨終值對象綁定,也可以與純右值對象綁定(這時往往自動生成一個臨時對象)。

C++語言在引入了右值引用之後,面臨着一個問題:如何讓編程者指出哪個對象具有臨終值?這有兩種顯式指定方法:如果函數(或運算符)的返回類型為右值引用,或者通過類型轉換如static_cast<Type&&>或者std::move()模板函數。

定義[編輯]

設X為任何一種數據類型,則定義X&&是到數據類型X的右值引用(rvalue reference to X)。傳統的引用類型X&被稱為左值引用(lvalue reference to X)。例如:

int i;
int &j=i; //傳統的左值引用數據類型的變量並初始化
int &&k=std::move(i);  //定義一個右值引用數據類型的變量並初始化。std::move定義於<utility>

語義[編輯]

右值引用是一種數據類型,既有右值性質,也有左值性質。[參1]在C++11中為右值引用專門定義了臨終值(eXpiring value)這一概念。[註7]右值引用很類似於傳統的左值引用。[參3]

例如:

int s=101;

int&& foo(){return static_cast<int&&>(s);} //返回值为右值引用

int main() {
 int i=foo();   //右值引用作为右值,在赋值运算符的右侧
 int&& j=foo(); //j是具名引用。因此运算符左侧的右值引用作为左值
 int* p=&j;     //取得j的内存地址
}

C++中,引用(reference)是指綁定到內存中的相應對象上。左值引用是綁定到左值對象上;右值引用是綁定到臨時對象上。這裡的左值對象是指可以通過取地址&運算符得到該對象的內存地址;而臨時對象是不能用取地址&運算符獲取到對象的內存地址。

C++11有如下引用綁定規則

  • 非常量左值引用(X& ):只能綁定到X類型的左值對象;
  • 常量左值引用(const X&):可以綁定到 X、const X類型的左值對象,或X、const X類型的右值;[註8]
  • 非常量右值引用(X&&):只能綁定到X類型的右值;
  • 常量右值引用(const X&&):可以綁定規定到X、const X類型的右值。

對上述引用綁定規則,值得注意的是:

  1. 常量左值引用、常量右值引用、非常量右值引用,在綁定到字面量(literal)時,實際上用字面量構造了一個對象,然後綁定到該對象上;
  2. 上述引用綁定規定所提到的右值,包含了純右值臨終值(xvalue)。即右值引用可以綁定到純右值(隱式自動構造的臨時對象),也可以綁定到臨終值對象。
  3. 綁定規則在調用重載函數,虛、實參數結合時起到決定作用,可以確定哪個版本的重載函數被調用。一些函數的形參為右值引用類型,因此可接受右值實參的綁定,如移動構造函數、移動賦值運算符、正常的成員函數如std::vector::push_back;另一些函數的形參為左值引用類型,因此可接受左值實參的綁定。一些函數模板的形參是廣義引用(universal reference,詳見下文),即可以接受左值對象綁定,也可以接受臨時對象綁定,需要在模板推導時來決定形參是左值引用還是右值引用,這給了一套函數模板以極大地靈活性,可以同時處理兩種引用類型,可以把函數參數的引用類型「完美轉發」(詳見下文)給被調用的實現函數。
  4. C++0X曾經規定右值引用可以綁定到左值對象上,但在C++11中取消了這一許可。

由於右值引用主要針對移動語義用來修改被引用的對象的內容,所以常量右值引用(const X&&)較少用到。

函數返回值是右值數據類型還是右值引用類型,區別在於前者是「傳值」,後者是「傳引用」可以修改被引用的對象。例如:

#include <iostream>
#include <utility>

int i = 101, j = 101;

int foo(){ return i; }
int&& bar(){ return std::move(i); }
void set(int&& k){ k = 102; }
int main()
{
	set(foo());
	std::cout << i << std::endl;
	set(bar());
	std::cout << i << std::endl; 	 
}

上述例子中的函數set中可以對類型為右值引用的形參k賦值,這是因為C++標準規定,具名的右值引用被當作左值[註9]這一規定的意義在於,右值引用本來是用於實現移動語義,因而需要綁定一個對象的內存地址,然後具有修改這一對象內容的權限,這些操作與左值綁定完全一樣。右值綁定與左值綁定的分別在於函數重載時的行為。對於移動構造成員函數與移動賦值運算符成員函數,其形、實參數結合時是按照右值引用處理;而在這兩個成語函數體內部,由於形參都是具名的,因而都被當作左值,這就可以用該形參來修改傳入對象的內部狀態。另外,右值引用作為xvalue(臨終值)本來是用於移動語義中一次性搬空其內容。具名使其具有更為持久的生存期,這是危險的,因而規定具名後為左值引用,除非程序顯式指定其類型強制轉換為右值引用。

C++11標準給了使用者更大的決定權,例如把左值或臨終值,轉化為右值引用。這是通過定義在C++標準程序庫<utility>中的std::move實現的。[參4]std::move是個模板函數,把輸入的左值或右值轉換為右值引用類型的臨終值。其核心是強制類型轉換static_cast<Type&&>()語句。

除了左值引用、右值引用,對於函數參數類型是到模板參數類型的右值引用的情形,稱之為廣義引用(universal reference)或轉發引用(forward reference)。[參4]例如:

 template<typename T> int foo(T&& param);

詳見完美轉發一節的討論。

完美轉發[編輯]

完美轉發也是C++11標準引入右值引用這一概念所要實現的目標之一。

背景[編輯]

在C++程序設計中,一個常見的類工廠函數,如下例:

template <typename T, typename Arg> 
shared_ptr<T> factory(Arg arg)
{
    return shared_ptr<T>( new T(arg) );
}

參數對象arg在上例中是傳值方式傳遞,這帶來了生成額外的臨時對象的代價。對於類工廠函數,完美的參數傳遞應該是引用方式傳遞。因而,在boost:bind中,參數是左值引用:

template <typename T, typename Arg> 
shared_ptr<T> factory(Arg& arg)
{
    return shared_ptr<T>( new T(arg) );
}

這種實現的問題是形參不能綁定右值實參。如factory<X>(102)將編譯報錯。進一步解決辦法是按常量引用方式傳遞參數,如下例:

template <typename T, typename Arg> 
shared_ptr<T> factory(const Arg& arg)
{
    return shared_ptr<T>( new T(arg) );
}

這種實現的問題是不能支持移動語義。

形參使用右值引用可以解決完美轉發問題。

引用摺疊規則[編輯]

對於C++語言,不可以在源程序中直接對引用類型再施加引用。T& &將編譯報錯。C++11標準中仍然禁止上述顯式對引用類型再施加引用,但如果在上下文環境中(包括模板實例化、typedef、auto類型推斷等)如出現了對引用類型再施加引用,則施行引用塌縮規則(reference collapsing rule)[註10]

  • T& &變為T&
  • T& &&變為T&
  • T&& &變為T&
  • T&& &&變為T&&

模板參數類型推導[編輯]

對函數模板template<typename T>void foo(T&&);,應用上述引用塌縮規則,可推導出如下結論:

  • 如果實參是類型A的左值,則模板參數T的類型為A&,形參類型為A&;
  • 如果實參是類型A的右值,則模板參數T的類型為A&&,形參類型為A&&。

這還適用於類模板的成員函數模板的類型推導:

template <class T > class vector {
    public: 
    void push_back(T&& x); // T是类模板参数 ⇒ 该成员函数不需要类型推导;这里的函数参数类型就是T的右值引用
     template <class Args> void emplace_back(Args&& args); //  该成员函数是个函数模板,有自己的模板参数,需要类型推导
};

函數模板的形參必須是T&&形式,才需要模板參數類型推導。即使形參聲明為const T&&形式,就只能按字面意義使用,不需要模板參數類型推導。

template<typename T>void f(const T&& param); // 这里的“&&”不需要类型推导,意味着“常量类型T的右值引用”
template<typename T>void f(std::vector<T>&& param);  // 这里的“&&”不需要类型推导,意味着std::vector<T>的右值引用

完美轉發的解決方案[編輯]

template <typename T, typename Arg> 
shared_ptr<T> factory(Arg&& arg)
{
    return shared_ptr<T>( new T(std::forward<Arg>(arg) ) );
}

其中std::forward是定義在C++標準程序庫<utility>中的模板函數:

template< class T > T&& forward( typename std::remove_reference<T>::type& t )
{
  return static_cast<T&&>(t);
}
template< class T > T&& forward( typename std::remove_reference<T>::type&& t )
{
  return static_cast<T&&>(t);
}

std::forward在調用時,應當顯示給出該函數模板參數類型。其用途是:如果函數forward的實參的數據類型是左值引用,則返回類型為左值引用;如果函數forward的實參的數據類型是右值引用,則返回類型為右值引用,返回值的分類屬於臨終值,從而把參數的信息完整地傳遞給下一級被調用的函數。從上述std::forward的定義實現來看,實參必須是個為左值的引用對象,但是實參的數據類型有兩種可能:

  • 實參的數據類型S是左值引用類型,std::forward的返回類型S&&根據引用塌縮規則變為S&,即返回值仍為左值引用類型;
  • 實參的數據類型S是右值引用類型(這是因為右值引用類型的具名變量實際上表現為左值),std::forward的返回類型S&&根據引用塌縮規則變為S&&,即返回值為右值引用類型。

類似的,std::move也是利用上述模板函數類型推導規則,定義在C++標準程序庫<utility>中,無論輸入的實參是左值還是右值,均返回右值引用:

template<class T>
typename std::remove_reference<T>::type&& move(T&& a) noexcept
{ 
  return static_cast<typename std::remove_reference<T>::type&&>(a);
}

定義在C++標準程序庫<type_traits>std::remove_reference是個類模板,其中定義的類型type是引用的基類型:

template< class T > struct remove_reference      {typedef T type;};
template< class T > struct remove_reference<T&>  {typedef T type;};
template< class T > struct remove_reference<T&&> {typedef T type;};

移動語義與異常[編輯]

移動語義只是把資源從一個右值對象搬移到被構造或者被賦值對象內部,因此保證不拋出異常是容易實現的。在std::vector這樣的容器內實現移動語義,必須顯式聲明容器元素類的移動構造成員函數、移動賦值運算符成員函數不拋出異常。

右值引用的類型推導[編輯]

C++中涉及到的右值引用的類型推導,除了上述模板參數類型推導,還有:

auto關鍵字的類型完美轉發[編輯]

C++11使用auto聲明變量時,如:auto&& var=initValue;「auto&&」並不意味着這一定是右值引用類型的變量,而是類似於模板函數參數的類型推導,既可能是左值引用,也可能是右值引用。其目的是把初始化表達式的值分類情況,完美轉發給由auto聲明的變量。也即:

  • 如果初始化值(initializer)是類型A的左值,則聲明的變量類型為左值引用A&;
  • 如果初始化值是類型A的右值,則聲明的變量類型為右值引用A&&。
Type1&& var1=anotherType1Instance; // var1的类型是右值引用,但是作为左值
auto&& var2=var1;       //var2的类型是左值引用
std::vector<int> v;
auto&& val = v[0]; // std::vector::operator[]的返回值是元素左值,所以val的类型是左值引用

Widget makeWidget(); // 类工厂函数
Widget&& var1 = makeWidget() // var1的类型是右值引用,具有左值。
     
Widget var2 = static_cast<Widget&&>(var1); // var2在初始化时可以使用移动构造函数。

typedef的類型推導[編輯]

typedef也可能會用到引用塌縮規則。例如:

template<typename T>    class Widget {
    typedef T& LvalueRefType;

};
Widget<int&&> w; // LvalueRefType的类型为int&
void f(Widget<int&>::LvalueRefType&& param); //param的类型为int&

decltype類型推導[編輯]

decltype也可能會用到引用塌縮規則。例如:

int var;
decltype(var)&& v1=std::move(var); //类型是int&&

備註[編輯]

    註:

  1. ^ 字面量可能會被編碼到機器指令的「立即數」中
  2. ^ 臨時對象可能會保存在寄存器中
  3. ^ 典型應用是STL的容器釋放內存時,與一個空的臨時容器對象swap內容,然後臨時容器對象出作用域而自動析構釋放。如: std::vector<int>().swap(myVector); //myVector已经分配了大量内存需要释放掉
  4. ^ 右值也可能通過對象的成員函數獲取到地址值。因此必須注意:是否能用&運算符取地址,才是區分表達式是左值與右值的關鍵。
  5. ^ 不包含作為sizeof運算符的操作數、作為取地址&運算符的操作數等情況。這些情況下,表達式中的數組名作為表示整個數組的左值使用
  6. ^ C++語言標準並不規定引用是如何實現的,但g++與Visual C++都是把引用實現為自動解引用的指針。例如,一個函數的參數是引用類型,用不同的對象作為實參調用該函數,在函數內部就可以通過形參訪問不同的對象。
  7. ^ C++03中的左值、右值分別對應於C++11的左值與右值
  8. ^ 因而常量左值引用類型的形參,可以與任何類型的實參結合。
  9. ^ 被稱作if-it-has-a-name rule.
  10. ^ 也譯作引用摺疊規則

參考文獻[編輯]