第14課 右值引用(1)_基本概念


1. 左值和右值

(1)兩者區別:

  ①左值:能對表達式取地址、或具名對象/變量。一般指表達式結束后依然存在的持久對象。

  ②右值:不能對表達式取地址,或匿名對象。一般指表達式結束就不再存在的臨時對象。

(2)右值的分類

  ①將亡值(xvalue,eXpiring value):指生命期即將結束的值,一般是跟右值引用相關的表達式,這樣表達式通常是將要被移動的對象,如返回類型為T&&的函數返回值(如std::move)、經類型轉換為右值引用的對象(如static_cast<T&&>(obj))、xvalue類對象的成員訪問表達式也是一個xvalue(如Test().memberdata,注意Test()是個臨時對象)

  ②純右值(prvalue, PureRvalue):按值返回的臨時對象運算表達式產生的臨時變對象原始字面量lambda表達式等。

(3)C++11中的表達式

 

  ①表達式是由運算符(operator)和運算對象(operand)構成的計算式。字面值和變量是最簡單的表達式函數的返回值也被認為是表達式

  ②表達式是可求值的,對表達式求值將得到一個結果這個結果有兩個屬性類型和值類別,而表達式的值類別必屬於左值、將亡值或純右值三者之一

  ③“左值”和“右值”是表達式結果的一種屬性。通常用“左值”來指代左值表達式,用“右值”指代右值表達式。

2. 右值引用和左值引用

(1)右值引用和左值引用

  ①右值引用和左值引用都是屬於引用類型。無論是聲明一個左值引用還是右值引用,都必須立即進行初始化

  ②左值引用是具名變量/對象的別名,右值引用是匿名變量/對象的別名。

  ③左值和右值是獨立於它的類型的,即左右值與類型沒有直接關系,它們是表達式的屬性具名的右值引用是左值,匿名的右值引用是右值。如Type&& t中t是個具名變量(最簡單的表達式),t的類型是右值引用類型,但具有左值屬性。而Type&& func()中的返回值(是個表達式)是右值引用類型,但具有右值屬性(因為是個匿名對象)。

(2)C++中引用類型及其可以引用的值類別

引用類型

可以引用的值類別

備注

非常量左值

常量左值

非常量右值

常量右值

Type&

Y

N

N

N

只能綁定到非常量左值

const Type&

Y

Y

Y

Y

萬能類型、用於拷貝語議

Type&&

N

N

Y

N

只能綁定到右值。用於移動語義和完美轉發

const Type&&

N

N

Y

Y

暫無用途

  ①常量左值引用是個“萬能”的引用類型。它可以接受非常量左值、常量左值、右值對其進行初始化。

  ②常量左值引用可以使用右值進行初始化,這時它可以像右值引用一樣將右值的生命期延長。不過相比於右值引用所引用的右值,常量左值所引用的右值在它的“余生”中只能是只讀的。

【編程實驗】左、右值引用

#include <iostream>
#include <type_traits>

//編譯選項:g++ -std=c++11 test1.cpp -fno-elide-constructors
using namespace std;

//左/右值以及左/右值引用

struct Test
{
    int m;

public:
    Test(){cout << "Test()" << endl;}
    Test(const Test& t){cout << "Test(const Test&)" << endl;}
    Test(Test&& t){cout << "Test(Test&&)" << endl;}
    
    ~Test(){cout << "~Test()" << endl;}
};

Test&& func()
{
    return Test(); //不安全!返回局部對象的引用(用於演示)!
}

Test ReturnRvalue()
{
    return Test(); 
}

int main()
{
    //1. 左、右值判斷
    int i = 0;
    int&& ri = i++; //i++為右值,表達式返回是i的拷貝,是個匿名變量。故為右值。
    int&  li = ++i; //++i返回的是i本身,是具名變量,故為左值。
    
    int* p = &i;
    int& lp = *p;    //*p是左值,因為可以取*p的地址,即&(*p);
    int* && rp = &i; //取地址表達式結果是個地址值,故&i是純右值。
    
    int&& xi1 = std::move(i); //std::move(i)是個xvalue
    int&& xi2 = static_cast<int&&>(i); // static_cast<int&&>(i)是個xvalue
    
    auto&& fn = [](int x){ return x * x; }; //lambda表達式是右值,可以用來初始化右值引用
    cout << std::is_rvalue_reference<decltype(fn)>::value << endl; //1
    
    Test t;
    int& rm1  = t.m; //由於t是左值,而m為普通成員變量,所以m也為左值。
    int&& rm2 = Test().m; //由於Test()是個右值,所以m也是右值
    
    int Test::*pm = &Test::m; //定義指向成員變量的指針,指向non-static member data;
    //int&& rm3 = t.*pm; //error,由於t是左值,*pm也是左值,不能用來初始化右值引用。
    int& rm3 = t.*pm;    //ok
    int&& rm4 = Test().*pm; //ok,Test()是臨時變量,為右值。所以*pm也是右值
    
    //2. 左/右值引用的初始化
    int a;
    int&  b = a;  //ok
    //int&& b = a;  //error,右值引用只能綁定到右值上    
    
    Test&& t1 = ReturnRvalue();  //返回值是個臨時對象(右值) 被綁定到t1上,使其“重獲新生”,
                                 //生命期與t1一樣。
    Test   t2 = ReturnRvalue();  //返回值是個臨時對象(右值),用於構造t2,之后該臨時對象
                                 //就會馬上被釋放。
    //Test&  t3 = ReturnRvalue();    //普通左值引用不能綁定到右值
    const Test& t4 = ReturnRvalue(); //常左值引用是個“萬能引用”,可以綁定到右值    
    
    
    //system("pause");
    return 0;
}

3. universal引用(T&&)

(1)T&&的兩種含義

  ①右值引用:當T是確定的類型時,T&&為右值引用。如int&& a;

  ②T存在類型推導時,T&&為universal引用,表示一個未定的引用類型。如果被右值初始化,則T&&為右值引用。如果被左值初始化,則T&&為左值引用

(2)引用折疊

  ①由於引用本身不是一個對象,C++標准不允許直接定義引用的引用。如“int& & a = b;”(注意兩個&中間有空格,不是int&&)這樣的語句是編譯不過的。

  ②類型推導時可能會間接地創建引用的引用,此時必須進行引用折疊。具體折疊規則如下:

    A. X& &、X& &&和X&& &都折疊成類型X&。即凡是有左值引用參與的情況下,最終的類型都會變成左值引用。

    B. 類型X&& &&折疊成X&&。即只有全部為右值引用的情況才會折疊為右值引用。

  ③引用折疊規則暗示我們,可以將任意類型的實參傳遞給T&&類型的函數模板參數。

(4)注意事項

  ①只有當發生自動類型推導時(如函數模板的類型自動推導或auto關鍵字),&&才是一個universal引用。當T的類型是確定的類型時,T&&為右值引用。

  ②當使用左值(類型為A)去初始化T&& t時,類型推導為A& &&,折疊會為A&,即t的類型為左值引用。而如果使用右值初始化T&&時,類型推導為A&&,一步到位無須折疊。

  ③universal引用僅僅在T&&下發生任何一點附加條件都會使之失效,而變成一個普通的右值引用(const T&&被const修飾就成了右值引用)

【編程實驗】引用折疊

#include <iostream>
#include <vector>
using namespace std;

//編譯選項:g++ -std=c++11 test2.cpp

class Widget{};

template<typename T>
class Foo
{
public:
    typedef T&& RvalueRefToT;  //RvalueRefToT為universal引用
};

//universal引用: T&&存在類型推導
template<typename T>
void func(T&& param)    //存在類型推導param為universal引用
{};

//非universal引用:形參必須是嚴格的T&&格式。
template<typename T>
void func(vector<T>&& param){}; //param是一個右值引用,因為當給func傳入實參時,T被推導后vector<T>&&的類型是確定的。
                                
//非universal引用:形參必須是嚴格的T&&格式。
//哪怕被const修飾也不行
template<typename T>
void func(const T&& param);  //param是一個右值引用

//比較universal引用和右值引用
//假設實例化后為:Vector<Widget> v;
template<class T, class Allocator=allocator<T>>
class Vector{

public:
    //雖然push_back的形參符合T&&格式,但不是universal引用因為Vector實例化后,push_back的形參類型就確定下來
    //所以在調用時push_back函數時並不存在類型推導。
    void push_back(T&& x);
    
    //Arg&&存在類型推導,所以args的參數是universal引用。因為參數Args獨立於vector的類型參數T,所以每次emplace_back被調用的時候,Args必須被推導。
    template<class ...Args>
    void emplace_back(Args&&...args);
};

int main()
{
    void f(Widget&& param);   //沒有類型推導,param為右值引用
    Widget&& var1 = Widget(); //沒有類型推導,var1為右值引用
    
    //1. auto中可能發生引用折疊
    auto&& var2 = var1;   //存在類型推導,var2為universal引用。var2被左值初始化,auto被推導為Widget& &&,折疊后為Widget&
    int w1, w2;
    auto&& v1 = w1;           //v1是universal引用。被左值初始化,所以其類型為int&
    auto&& v2 = std::move(w1);//v2為universal引用,被右值初始化,所以類型為int&&
    
    //2. decltype中可能發生引用折疊:decltype(x)會先取出x的類型,再通過引用折疊規則來定義變量
    decltype(v1)&& v3 = w2;            //v1的類型為int&,所以v3為int& &&,折疊后為int&
    decltype(v2)&& v4 = std::move(w2); //v2的類型為int&&,所以v4為int&& &&,折疊后為int&&
    decltype(w1)&& v5 = std::move(w2); //w1的類型為int,所以v5為int&&
    
    //3. typedef時可能發生的引用折疊
    Foo<int&> f1;  //==>typedef int& && RvalueRefToT; 折疊后:typedef int& RvalueRefToT;
    Foo<int&&> f2; //typedef int&& && RvalueRefToT;   折疊后:typedef int&& RvalueRefToT;
    
    int i = 0;
    int& r1 = i; //ok
    //int& &r2 = r1; //error,不能直接定義引用的引用
    
    typedef int& rint;
    rint r2 = i; //ok,r1為int&類型
    rint &r3 = i; //間接定義引用的引用時,會發生引用折疊。如int& &r3 ==>int& r3
    
    //4. 函數模板參數中可能發生引用折疊
    Widget w;
    func(w);   //用左值w初始化T&&,存在類型推導T為Widget&,傳入func后為Widget& &&,折疊后為Widget&,所以param的類型為Widget&,是個左值引用
    func(std::move(w)); //用右值初始化T&&,T被推導為Widget,傳入
                        //func后為Widget&&,所以param為右值引用。
    vector<int> v;                
    func(std::move(v));   //調用右值引用的版本:func(vector<T>&& param)。
    
    return 0;
}

4. &&的總結

(1)左值和右值是表達式的屬性,獨立於它們的類型。比如,右值引用類型可能是左值也可能是右值。編譯器將具名的右值引用視為左值,匿名的右值引用視為右值

(2)auto&&或函數參數存在類型推導時,T&&是一個未定的引用類型。它可能是左值引用,也可能是右值引用,取決於初始化的值類型。

(3)所有的右值引用疊加到右值引用上仍然是一個右值引用,其它種疊加都是左值引用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM