1. 程式人生 > >C++左值與右值,移動與完美轉發

C++左值與右值,移動與完美轉發

左值與右值

判別:

  • 左值:用來儲存資料的變數,有實際的記憶體地址,表示式結束後任然存在。
  • 右值:匿名的臨時變數,表示式結束時被銷燬,不能存放資料,可以被修改或者不修改;字面常量也是右值。
int x=10;
int* p=&++x;  // 正確,前置++返回左值
int* q=&x++;  // 錯誤,後置++返回臨時物件

左值引用就是普通的引用,下面介紹右值引用:

#include <iostream>
using namespace std;

int main() {
    int x = 1;
    int&& r = x++;
    cout <<
r << endl; // 1 cout << x << endl; // 2 x = 10; cout << r << endl; // 1 return 0; }

引用了臨時的右值物件,相當於延長了生命週期。

std::move()移動

轉移語義,使用的std::move()進行轉移物件,把一個物件(左值)轉移成匿名的右值。它的意義在於減少不必要的拷貝操作,提高程式效能。本質上,std::move是使得物件的所有權發生了轉移,給出程式碼例項

#include <iostream>
#include
<vector>
#include <string> int main() { std::string str = "Hello"; std::vector<std::string> v; // 這裡是對str的一個拷貝,拷貝了字串的副本到vector中 v.push_back(str); std::cout << "After copy, str is \"" << str << "\"\n"; // 這裡是直接把str內容轉移到vector中,沒發生任何拷貝 // 原來的左值被轉移後,str又成為了空串
v.push_back(std::move(str)); std::cout << "After move, str is \"" << str << "\"\n"; std::cout << "The contents of the vector are \"" << v[0] \ << "\", \"" << v[1] << "\"\n"; return 0; } /* 程式碼輸出: After copy, str is "Hello" After move, str is "" The contents of the vector are "Hello", "Hello" */

上述程式碼解決了一個效率問題,每次不用拷貝一遍字串到vector中,而是直接使用原來的字串。注意到,容器的本身不能儲存元素的引用,這在處理複雜物件的時候,會進行大量的拷貝,浪費時間和記憶體;但是,藉助於std::move可以直接把左值物件的值進行轉移,從而省去大量的複製步驟!

對於自定義的物件,需要顯示的說明轉移建構函式,程式碼例項

#include <iostream>
#include <vector>
#include <string>

class Node {
  public:
    // 預設無引數建構函式
    Node() {
        std::cout << "default construction" << std::endl;
        a = 0;
        str = "";
        p = nullptr;
    }
    //  普通的拷貝函式
    Node(const Node& node) {
        std::cout << "copy construction" << std::endl;
        a = node.a;
        str = node.str;
        p = node.p;
    }
    // 移動建構函式,注意不能宣告為const !!! 為確保安全,宣告為不丟擲異常的,
    // 移動失敗後直接退出程式,否則有懸空指標是非常危險的。
    Node(Node&& node) noexcept {
        std::cout << "move construction" << std::endl;
        a = std::move(node.a);
        str = std::move(node.str); // 注意使用move提高效率,string型別是可以直接move的
        p = node.p;
        node.p = nullptr;  // 置空原來的指標
    }
    // 解構函式
    ~Node() {
        std::cout << "deconstruction" << std::endl;
        a = 0;
        str.clear();
        if(!p) {
            delete p;
            p = nullptr;
        }
    }
    int a;
    int* p;
    std::string str;
};

int main() {
    Node n; // 正常的預設建構函式
    n.str = "hello world !";
    n.a = 10;
    n.p = new int(10);
    Node n1(n);                // 這裡執行拷貝操作
    std::vector<Node>vec;
    vec.push_back(std::move(n1)); // 這裡執行move操作
    std::cout << "n.str= " << n.str << std::endl;
    std::cout << "n1.str= " << n1.str << std::endl;
    std::cout << "vec.str= " << vec[0].str << std::endl;
    return 0;
}
/*
輸出結果:
default construction
copy construction
move construction
n.str= hello world !
n1.str=
vec.str= hello world !
deconstruction
deconstruction
deconstruction
*/

說明一點,std::move完成後,原來的左值不會立刻析構,而是正常流程的結束並析構。

std::forward完美轉發

C++的std::forward完美轉發,在函式模板中,完全依照模板的引數的型別,將引數傳遞給函式模板中呼叫的另外一個函式。如果傳入的引數是不是左值引用,那麼返回一個引數右值的引用;如果引數是左值引用,那麼返回左值的引用。

完美轉發的一般用途:

template<typename T>
void IamForwording(T t) {
    IrunCodeActually(t);
}

上面的程式碼模板說明,IamForwording函式的用途只是把模板引數t傳入進來,而IrunCodeActually是真正執行的函式,該函式希望原封不動傳遞前者傳入引數的型別。

為了處理各種引數的匹配關係,C++引入了引數摺疊規則,給出編譯推斷的策略:

T& + & => T&
T&& + & => T&
T& + && => T&
T&& + && => T&&

+左側是函式形參表示的形式,+右側是實際傳入引數的形式,=>後表示實際推斷的形式。

雖然組合方式很多,但是有一個規律:只有形參和傳入的引數同時是右值時,才會推斷成右值引用;否則一律是左值

因此,引入std::forward來解決這個問題。

template <class T> T&& forward (typename remove_reference<T>::type& arg) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& arg) noexcept;

函式的返回值是:如果arg是左值,就返回左值;否則一律返回右值。

給出一個簡單的例子:

給出C++參考的例項測試:

#include <utility>
#include <iostream>

void overloaded(const int& x) {
    std::cout << "lvalue\n";
}

void overloaded(int&& x) {
    std::cout << "rvalue\n";
}

template<typename T>
void fn(T&& x) {
    overloaded(x);
    overloaded(std::forward<T>(x));
}

int main() {
    int a;
    std::cout << "calling fn with lvalue:\n";
    fn(a);
    std::cout << "calling fn with rvalue:\n";
    fn(0);
    return 0;
}
/*
輸出結果:
calling fn with lvalue:
lvalue
lvalue
calling fn with rvalue:
lvalue
rvalue
*/

從結果可以看出,使用了std::forward的函式引數才能原封不動的傳遞原來資料的型別。這樣可以根據引數的型別,自動的進行不同的過載。