C++右值引用

Kayden_Cheung發表於2022-02-25

 

左值、右值

1 左值是表示式結束後依然存在的持久物件

2 右值是表示式結束後不再存在的臨時物件

簡單來說,能取地址的是左值,否則就是右值。

 

右值引用的意義

實現移動語義完美轉發

 

移動語義

C++11的右值引用和std::move可以實現移動語義,通過減少拷貝操作提升效率

class A{

public:
    A(): size(0), data(nullptr){}
    A(size_t size): size(size){
        data = new int[size];
    }

    A(const A& rhs){
        size = rhs.size;
        data = new int[size];
        for(int i=0; i<size; ++i){
            data[i] = rhs.data[i];
        }
        cout << "Copy constructor" << endl;
    }

    A& operator=(const A& rhs){

        if(this == &rhs){
            return *this;
        }

        delete[] data;

        size = rhs.size;
        data = new int[size];
        for(int i=0; i<size; ++i){
            data[i] = rhs.data[i];
        }
        cout << "Copy assignment" << endl;
        return *this;
    }


    ~A(){
        delete[] data;
    }

private:
    int *data;
    size_t size;
};

對於上面的類來說,無論是拷貝構造還是拷貝賦值,都需要將源資料一一拷貝,但是如果源資料在拷貝之後就不需要了,我們直接把源資料拿過來不是省事多了嗎?對此我們只需要修改一下指標地址即可,在程式碼中加入下面的移動建構函式。

A(A&& rhs){
    data = rhs.data;
    size = rhs.size;
    rhs.size = 0;
    rhs.data = nullptr;
    cout << "Move constructor" << endl;
}

於是通過std::move將源資料轉化為右值後就會呼叫相應的移動建構函式。

int main(){
    A a1 = A(10);
    A a2(std::move(a1));
}

 

完美轉發

首先看第一個例子:

#include<iostream>
using namespace std;

int main(){
    int x = 5;
    int& left = x;
    int&& right = std::move(x);
    //cout << &left << endl << &right << endl;
}

在上面的程式碼中,被宣告的左值引用和右值引用都是左值,是可以輸出它們的地址的。

 

然後看二個例子:

#include<iostream>
using namespace std;

void func(int& x){
    cout << "左值" <<endl;
}

void func(int&& x){
    cout << "右值" <<endl;
}

void test(int&& x){
    func(x);
}

int main(){
    test(5);
}

在上面的程式碼中,最後的輸出是左值,雖然傳給 test 函式的是一個右值,然而在呼叫 func 時,使用的是 x,引數有了名稱,聯絡第一個例子,此時 x 是一個左值。

如果想要最後的輸出是右值,需要使用 std::forward,而 std::forward 中涉及到了引用摺疊。

 

引用摺疊

對於T&、T&&來說,其引用型別未定,可能是左值引用,也可能是右值引用,需要視傳入的實參而定。

T& &  T& 左值引用的左值引用摺疊為左值引用
T& && T& 右值引用的左值引用摺疊為左值引用
T&& & T& 左值引用的右值引用摺疊為左值引用
T&& && T&& 右值引用的右值引用摺疊為右值引用

簡而言之,只有兩者均為右值引用時,最後的引用型別才為右值引用,否則均為左值引用

 

原始碼分析

 remove_reference 原始碼

template<typename _Tp>
struct remove_reference
{ typedef _Tp   type; };
 
// 特化版本
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp   type; };
 
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp   type; };

std::forward 原始碼

template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }

根據上述原始碼,首先通過remove_reference獲取 _Tp 的型別 type,然後宣告左值引用變數 __t 。

根據 _Tp 的不同,_Tp 會發生引用摺疊:

  1. 當 _Tp 為左值引用時,_Tp摺疊為左值引用
  2. 當 _Tp 為右值引用時,_Tp摺疊為右值引用

可以發現當 std::forward 的輸入是左值引用時,輸出也是左值引用;輸入是右值引用時,輸出也是右值引用。

 

在下面的程式碼中,使用了 std::forward 之後就會輸出右值。

#include<iostream>
using namespace std;

void func(int& x){
    cout << "左值" <<endl;
}

void func(int&& x){
    cout << "右值" <<endl;
}

void test(int&& x){
    func(std::forward<int>(x));
}

int main(){
    test(5);
}

  

 

 

References:

  1. 一文帶你詳細介紹c++中的std::move函式
  2. C++ 理解std::forward完美轉發
  3. 一文讀懂C++右值引用和std::move

相關文章