溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

C++11右值引用和移動(dòng)語(yǔ)義的方法是什么

發(fā)布時(shí)間:2023-02-23 11:28:19 來源:億速云 閱讀:87 作者:iii 欄目:開發(fā)技術(shù)

本文小編為大家詳細(xì)介紹“C++11右值引用和移動(dòng)語(yǔ)義的方法是什么”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“C++11右值引用和移動(dòng)語(yǔ)義的方法是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識(shí)吧。

    左值引用與右值引用

    1、左值與右值

    概念1:

    • 左值:可以放到等號(hào)左邊的東西叫左值。

    • 右值:不可以放到等號(hào)左邊的東西就叫右值。

    概念2

    • 左值:可以取地址并且有名字的東西就是左值。

    • 右值:不能取地址的沒有名字的東西就是右值。

    概念3

    • 左值是指那些在表達(dá)式執(zhí)行結(jié)束后依然存在的數(shù)據(jù),也就是持久性的數(shù)據(jù)。

    • 右值是指那些在表達(dá)式執(zhí)行結(jié)束后不再存在的數(shù)據(jù),也就是臨時(shí)性的數(shù)據(jù)。

    有一種很簡(jiǎn)單的方法來區(qū)分左值和右值:對(duì)表達(dá)式取地址,如果編譯器不報(bào)錯(cuò)就為左值,否則為右值。例如:int a = b + c;,a 是左值,有變量名,可以取地址,也可以放到等號(hào)左邊,表達(dá)式 b+c 的返回值是右值,沒有名字且不能取地址,&(b+c) 不能通過編譯,而且也不能放到等號(hào)左邊。

    左值一般有:

    • 變量名和函數(shù)名(注意:是函數(shù)名不是函數(shù)調(diào)用)

    • 返回左值引用的函數(shù)調(diào)用

    • 前置自增自減表達(dá)式++i、–i

    • 由賦值表達(dá)式或賦值運(yùn)算符連接的表達(dá)式(a=b, a += b等)

    • 解引用表達(dá)式 *p

    • 字符串字面值 “abcd”

    2、純右值、將亡值

    純右值和將亡值都屬于右值。

    純右值:運(yùn)算表達(dá)式產(chǎn)生的臨時(shí)變量、不和對(duì)象關(guān)聯(lián)的原始字面量、非引用返回的臨時(shí)變量、lambda 表達(dá)式等都是純右值。舉例:

    • 除字符串字面值外的字面值

    • 返回非引用類型的函數(shù)調(diào)用

    • 后置自增自減表達(dá)式 i++、i–

    • 算術(shù)表達(dá)式 a+b,a*b,a&&b,a==b 等

    • 取地址表達(dá)式等,&a

    將亡值:

    將亡值是指 c++11 新增的和右值引用相關(guān)的表達(dá)式,通常指將要被移動(dòng)的對(duì)象、T&& 函數(shù)的返回值、std::move函數(shù)的返回值、轉(zhuǎn)換為 T&& 類型轉(zhuǎn)換函數(shù)的返回值,將亡值可以理解為即將要銷毀的值,通過“盜取”其它變量?jī)?nèi)存空間方式獲取的值,在確保其它變量不再被使用或者即將被銷毀時(shí),可以避免內(nèi)存空間的釋放和分配,延長(zhǎng)變量值的生命周期,常用來完成移動(dòng)構(gòu)造或者移動(dòng)賦值的特殊任務(wù)。舉例:

    class A {
        xxx;
    };
    
    A a;
    auto c = std::move(a);         // c是將亡值
    auto d = static_cast<A&&>(a);  // d是將亡值

    3、左值引用與右值引用

    左值引用就是對(duì)左值進(jìn)行引用的類型,右值引用就是對(duì)右值進(jìn)行引用的類型,他們都是引用,都是對(duì)象的一個(gè)別名,并不擁有所綁定對(duì)象的堆存,所以都必須立即初始化。引用可以通過引用修改變量的值,傳參時(shí)傳引用可以避免拷貝。

    type &name = exp;  // 左值引用
    type &&name = exp; // 右值引用

    左值引用

    左值引用:能指向左值,不能指向右值的就是左值引用:

    int a = 5;
    int& b = a;  // b是左值引用
    b = 4;
    
    int& c = 10;  // error,10無法取地址,無法進(jìn)行引用
    const int& d = 10;  // ok,因?yàn)槭浅R?,引用常量?shù)字,這個(gè)常量數(shù)字會(huì)存儲(chǔ)在內(nèi)存中,可以取地址。

    引用是變量的別名,由于右值沒有地址,沒法被修改,所以左值引用無法指向右值,等號(hào)右邊的值必須可以取地址,如果不能取地址,則會(huì)編譯失敗。

    但是,const 左值引用(常量引用)是可以指向右值的:const 左值引用不會(huì)修改指向值,因此可以指向右值,這也是為什么要使用 const & 作為函數(shù)參數(shù)的原因之一。

    右值引用

    c++11 標(biāo)準(zhǔn)新引入了另一種引用方式,稱為右值引用,用 “&&” 表示。如果使用右值引用,那表達(dá)式等號(hào)右邊的值需要是右值(不能是左值),可以使用 std::move 函數(shù)強(qiáng)制把左值轉(zhuǎn)換為右值。

    int a = 4;
    int&& b = a;             // error, a 是左值
    int&& c = std::move(a);  // ok
    
    int num = 10;
    int && a = num;         //error, 右值引用不能初始化為左值
    int && a = 10;          // ok

    【注意】和聲明左值引用一樣,右值引用也必須立即進(jìn)行初始化操作。

    左值引用與右值引用本質(zhì)

    (1)右值引用指向左值

    int a = 5; // a是個(gè)左值
    int &ref_a_left = a; // 左值引用指向左值
    int &&ref_a_right = std::move(a); // 通過std::move將左值轉(zhuǎn)化為右值,可以被右值引用指向
    cout << a; // 打印結(jié)果:5

    前面講過可以使用 std::move 函數(shù)強(qiáng)制把左值轉(zhuǎn)換為右值,實(shí)現(xiàn)右值引用指向左值。std::move 是一個(gè)非常有迷惑性的函數(shù):

    • 不理解左右值概念的人們往往以為它能把一個(gè)變量里的內(nèi)容移動(dòng)到另一個(gè)變量,比如在上邊的代碼里,看上去是左值 a 通過 std::move 移動(dòng)到了右值 ref_a_right 中,那是不是a里邊就沒有值了?并不是,打印出a的值仍然是5。

    • 事實(shí)上 std::move 移動(dòng)不了什么,唯一的功能是把左值強(qiáng)制轉(zhuǎn)化為右值,讓右值引用可以指向左值。其實(shí)現(xiàn)等同于一個(gè)類型轉(zhuǎn)換: static_cast<T&&>(lvalue)。 所以,單純的 std::move(xxx) 不會(huì)有性能提升。

    同樣的,右值引用能指向右值,本質(zhì)上也是把右值提升為一個(gè)左值,并定義一個(gè)右值引用通過 std::move:

    int &&ref_a = 5;
    ref_a = 6;
    
    // 等同于以下代碼:
    int temp = 5;
    int &&ref_a = std::move(temp);
    ref_a = 6;

    (2)左值引用、右值引用本身是左值還是右值?

    被聲明出來的左、右值引用都是左值。 因?yàn)楸宦暶鞒龅淖笥抑狄檬怯械刂返?,也位于等?hào)左邊。仔細(xì)看下邊代碼:

    // 形參是個(gè)右值引用
    void change(int &&right_value) { right_value = 8; }
    int main() {
        int a = 5;                         // a是個(gè)左值
        int &ref_a_left = a;               // ref_a_left是個(gè)左值引用
        int &&ref_a_right = std::move(a);  // ref_a_right是個(gè)右值引用
        change(a);                         // 編譯不過,a是左值,change參數(shù)要求右值
        change(ref_a_left);                // 編譯不過,左值引用ref_a_left本身也是個(gè)左值
        change(ref_a_right);             // 編譯不過,右值引用ref_a_right本身也是個(gè)左值
        change(std::move(a));            // 編譯通過
        change(std::move(ref_a_right));  // 編譯通過
        change(std::move(ref_a_left));   // 編譯通過
        change(5);                       // 當(dāng)然可以直接接右值,編譯通過
        cout << &a << ' ';
        cout << &ref_a_left << ' ';
        cout << &ref_a_right;
        // 打印這三個(gè)左值的地址,都是一樣的
    }

    看完后你可能有個(gè)問題,std::move 會(huì)返回一個(gè)右值引用 int && ,它是左值還是右值呢? 從表達(dá)式 int &&ref = std::move(a) 來看,右值引用 ref 指向的必須是右值,所以move返回的 int && 是個(gè)右值。所以右值引用既可能是左值,又可能是右值嗎? 確實(shí)如此:右值引用既可以是左值也可以是右值,如果有名稱則為左值,否則是右值。

    或者說:作為函數(shù)返回值的 && 是右值,直接聲明出來的 && 是左值。 這同樣也符合前面章節(jié)對(duì)左值,右值的判定方式:其實(shí)引用和普通變量是一樣的, int &&ref = std::move(a) 和 int a = 5 沒有什么區(qū)別,等號(hào)左邊就是左值,右邊就是右值。

    (3)無論是左值引用還是右值引用都是引用

    int temp = 5;
    int &ref_t = temp;
    int &&ref_a = std::move(temp);
    ref_a = 6;
    cout << &temp << "," << &ref_t << "," << &ref_a<<endl;
    cout << "temp:" <<temp <<endl;
    // 輸出結(jié)果
    // 0x61fe84  0x61fe84  0x61fe84 
    // temp:6

    最后,從上述分析中我們得到如下結(jié)論:

    • 從性能上講,左右值引用沒有區(qū)別,傳參使用左右值引用都可以避免拷貝。

    • 右值引用可以直接指向右值,也可以通過 std::move 指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。

    • 作為函數(shù)形參時(shí),右值引用更靈活。雖然 const 左值引用也可以做到左右值都接受,但它無法修改,有一定局限性。

    void f(const int& n) {
        n += 1;  // 編譯失敗,const左值引用不能修改指向變量
    }
    
    void f2(int&& n) {
        n += 1;  // ok
    }
    
    int main() {
        f(5);
        f2(5);
    }

    4、右值引用和 std::move 使用場(chǎng)景

    std::move 只是類型轉(zhuǎn)換工具,不會(huì)對(duì)性能有好處;右值引用在作為函數(shù)形參時(shí)更具靈活性。他們有什么實(shí)際應(yīng)用場(chǎng)景嗎?

    1、右值引用優(yōu)化性能,避免深拷貝

    (1)淺拷貝重復(fù)釋放:對(duì)于含有堆內(nèi)存的類,我們需要提供深拷貝的拷貝構(gòu)造函數(shù),如果使用默認(rèn)構(gòu)造函數(shù),會(huì)導(dǎo)致堆內(nèi)存的重復(fù)刪除,比如下面的代碼:

    class A {
    public:
        A(int size) : size_(size) { data_ = new int[size]; }
        A() {}
        A(const A& a) {
            size_ = a.size_;
            data_ = a.data_;
            cout << "copy " << endl;
        }
        ~A() { delete[] data_; }
        
        int* data_;
        int size_;
    };
    
    int main() {
        A a(10);
        A b = a;
        cout << "b " << b.data_ << endl;
        cout << "a " << a.data_ << endl;
        return 0;
    }

    上面代碼中,兩個(gè)輸出的是相同的地址,a 和 b 的 data_ 指針指向了同一塊內(nèi)存,這就是淺拷貝,只是數(shù)據(jù)的簡(jiǎn)單賦值,那再析構(gòu)時(shí) data_ 內(nèi)存會(huì)被釋放兩次,導(dǎo)致程序出問題,這里正常會(huì)出現(xiàn) double free 導(dǎo)致程序崩潰的。

    (2)深拷貝構(gòu)造函數(shù)

    在上面的代碼中,默認(rèn)構(gòu)造函數(shù)是淺拷貝,在析構(gòu)的時(shí)候會(huì)導(dǎo)致重復(fù)刪除指針。正確的做法是提供深拷貝的拷貝構(gòu)造函數(shù),比如下面的代碼:

    #include <iostream>
    using namespace std;
    class A {
    public:
        A() : m_ptr(new int(0)) { cout << "constructor A" << endl; }
        A(const A& a) : m_ptr(new int(*a.m_ptr)) {
            cout << "copy constructor A" << endl;
        }
        ~A() {
            cout << "destructor A, m_ptr:" << m_ptr << endl;
            delete m_ptr;
            m_ptr = nullptr;
        }
    private:
        int* m_ptr;
    };
    
    // 為了避免返回值優(yōu)化,此函數(shù)故意這樣寫
    A Get(bool flag) {
        A a;
        A b;
        cout << "ready return" << endl;
        if (flag)
            return a;
        else
            return b;
    }
    
    int main() {
        {
            A a = Get(false);  // 正確運(yùn)行
        }
        cout << "main finish" << endl;
        return 0;
    }

    深拷貝就是在拷貝對(duì)象時(shí),如果被拷貝對(duì)象內(nèi)部還有指針引用指向其它資源,自己需要重新開辟一塊新內(nèi)存存儲(chǔ)資源,而不是簡(jiǎn)單的賦值。雖然深拷貝可以解決淺拷貝的問題,但是存在效率問題。

    (3)移動(dòng)構(gòu)造函數(shù)

    深拷貝構(gòu)造函數(shù)可以保證拷貝構(gòu)造時(shí)的安全性,但有時(shí)這種拷貝構(gòu)造存在效率問題,比如上面代碼中的拷貝構(gòu)造就是不必要的。上面代碼中的 Get 函數(shù)會(huì)返回臨時(shí)變量,然后通過這個(gè)臨時(shí)變量拷貝構(gòu)造了一個(gè)新的對(duì)象 b,臨時(shí)變量在拷貝構(gòu)造完成之后就銷毀了,如果堆內(nèi)存很大,那么,這個(gè)拷貝構(gòu)造的代價(jià)會(huì)很大,帶來了額外的性能損耗。

    有沒有辦法避免臨時(shí)對(duì)象的拷貝構(gòu)造呢?答案是肯定的??聪旅娴拇a:

    #include <iostream>
    using namespace std;
    class A {
    public:
        A() : m_ptr(new int(0)) { cout << "constructor A" << endl; }
        A(const A& a) : m_ptr(new int(*a.m_ptr)) {
            cout << "copy constructor A" << endl;
        }
        // 移動(dòng)構(gòu)造函數(shù),可以淺拷貝
        A(A&& a) : m_ptr(a.m_ptr) {
            a.m_ptr = nullptr;  // 為防止a析構(gòu)時(shí)delete data,提前置空其m_ptr
            cout << "move constructor A" << endl;
        }
        ~A() {
            cout << "destructor A, m_ptr:" << m_ptr << endl;
            if (m_ptr) delete m_ptr;
        }
    private:
        int* m_ptr;
    };
    
    // 為了避免返回值優(yōu)化,此函數(shù)故意這樣寫
    A Get(bool flag) {
        A a;
        A b;
        cout << "ready return" << endl;
        if (flag)
            return a;  
        else
            return b;
    }
    
    int main() {
        {
            A a = Get(false);  // 返回右值,調(diào)用移動(dòng)構(gòu)造函數(shù)
        }
        cout << "main finish" << endl;
        return 0;
    }

    上面的代碼中實(shí)現(xiàn)了移動(dòng)構(gòu)造( Move Construct)。從移動(dòng)構(gòu)造函數(shù)的實(shí)現(xiàn)中可以看到,它的參數(shù)是一個(gè)右值引用類型的參數(shù) A&&,這里沒有深拷貝,只有淺拷貝,這樣就避免了對(duì)臨時(shí)對(duì)象的深拷貝,提高了性能。

    在實(shí)際開發(fā)中,通常在類中自定義移動(dòng)構(gòu)造函數(shù)的同時(shí),會(huì)再為其自定義一個(gè)適當(dāng)?shù)目截悩?gòu)造函數(shù),由此當(dāng)用戶利用右值初始化類對(duì)象時(shí),會(huì)調(diào)用移動(dòng)構(gòu)造函數(shù);使用左值(非右值)初始化類對(duì)象時(shí),會(huì)調(diào)用拷貝構(gòu)造函數(shù)。

    這里的 A&& 用來根據(jù)參數(shù)是左值還是右值來建立分支,如果是臨時(shí)值,則會(huì)選擇移動(dòng)構(gòu)造函數(shù)。

    移動(dòng)構(gòu)造函數(shù)只是將臨時(shí)對(duì)象的資源做了淺拷貝,不需要對(duì)其進(jìn)行深拷貝,從而避免了額外的拷貝,提高性能。這也就是所謂的移動(dòng)語(yǔ)義( move 語(yǔ)義),右值引用的一個(gè)重要目的是用來支持移動(dòng)語(yǔ)義的(移動(dòng)語(yǔ)義的分析詳細(xì)見下文)。

    引用限定符

    將左值的類對(duì)象稱為左值對(duì)象,將右值的類對(duì)象稱為右值對(duì)象。默認(rèn)情況下,對(duì)于類中用 public 修飾的成員函數(shù),既可以被左值對(duì)象調(diào)用,也可以被右值對(duì)象調(diào)用,舉個(gè)例子:

    #include <iostream>
    using namespace std;
    
    class demo {
    public:
        demo(int num) : num(num) {}
        int get_num() { return this->num; }
    
    private:
        int num;
    };
    
    int main() {
        demo a(10);
        cout << a.get_num() << endl;
        cout << move(a).get_num() << endl;
        return 0;
    }

    可以看到,demo 類中的 get_num() 成員函數(shù)既可以被 a 左值對(duì)象調(diào)用,也可以被 move(a) 生成的右值 demo 對(duì)象調(diào)用,運(yùn)行程序會(huì)輸出兩個(gè) 10。

    某些場(chǎng)景中,我們可能需要限制調(diào)用成員函數(shù)的對(duì)象的類型(左值還是右值),為此 c++11 新添加了引用限定符。所謂引用限定符,就是在成員函數(shù)的后面添加 “&” 或者 “&&”,從而限制調(diào)用者的類型(左值還是右值)?!咀⒁狻恳孟薅ǚ贿m用于靜態(tài)成員函數(shù)和友元函數(shù)。

    // 代碼修改
    class demo {
    public:
        demo(int num) : num(num) {}
        int get_num() & { return this->num; }  // 添加了 "&",限定調(diào)用該函數(shù)的對(duì)象必須是左值對(duì)象
    
    private:
        int num;
    };
    
    int main() {
        demo a(10);
        cout << a.get_num() << endl;  // 正確
        // cout << move(a).get_num() << endl;  // 錯(cuò)誤
        return 0;
    }
    // 代碼修改
    class demo {
    public:
        demo(int num) : num(num) {}
        int get_num() && { return this->num; }  // 添加了 "&&",限定調(diào)用該函數(shù)的對(duì)象必須是右值對(duì)象
    
    private:
        int num;
    };
    
    int main() {
        demo a(10);
        //cout << a.get_num() << endl; // 錯(cuò)誤
        cout << move(a).get_num() << endl; // 正確
        return 0;
    }

    const 和引用限定符

    const 也可以用于修飾類的成員函數(shù),習(xí)慣稱為常成員函數(shù)。

    const 和引用限定符修飾類的成員函數(shù)時(shí),都位于函數(shù)的末尾。C++11 標(biāo)準(zhǔn)規(guī)定,當(dāng)引用限定符和 const 修飾同一個(gè)類的成員函數(shù)時(shí),const 必須位于引用限定符前面。如下:

    #include <iostream>
    using namespace std;
    class demo {
    public:
        demo(int num, int num2) : num(num), num2(num2) {}
        //左值和右值對(duì)象都可以調(diào)用
        int get_num() const& { return this->num; }
        //僅供右值對(duì)象調(diào)用
        int get_num2() const&& { return this->num2; }
    
    private:
        int num;
        int num2;
    };

    【注意】當(dāng) const && 修飾類的成員函數(shù)時(shí),調(diào)用它的對(duì)象只能是右值對(duì)象;當(dāng) const & 修飾類的成員函數(shù)時(shí),調(diào)用它的對(duì)象既可以是左值對(duì)象,也可以是右值對(duì)象。無論是 const && 還是 const & 限定的成員函數(shù),內(nèi)部都不允許對(duì)當(dāng)前對(duì)象做修改操作。

    移動(dòng)語(yǔ)義&mdash;std::move()

    所謂移動(dòng)語(yǔ)義,指的就是以移動(dòng)而非深拷貝的方式初始化含有指針成員的類對(duì)象:之前的拷貝是對(duì)于別人的資源,自己重新分配一塊內(nèi)存存儲(chǔ)復(fù)制過來的資源,而對(duì)于移動(dòng)語(yǔ)義,類似于轉(zhuǎn)讓或者資源竊取的意思,對(duì)于那塊資源,轉(zhuǎn)為自己所擁有,別人不再擁有也不會(huì)再使用,通過 c++11 新增的移動(dòng)語(yǔ)義可以省去很多拷貝負(fù)擔(dān),怎么利用移動(dòng)語(yǔ)義呢,是通過移動(dòng)構(gòu)造函數(shù)。

    移動(dòng)語(yǔ)義可以將資源(堆、系統(tǒng)對(duì)象等)通過淺拷貝方式從一個(gè)對(duì)象轉(zhuǎn)移到另一個(gè)對(duì)象,這樣能夠減少不必要的臨時(shí)對(duì)象的創(chuàng)建、拷貝以及銷毀,可以大幅度提高 c++ 應(yīng)用程序的性能,消除臨時(shí)對(duì)象的維護(hù)(創(chuàng)建和銷毀)對(duì)性能的影響。

    class A {
    public:
        A(int size) : size_(size) { data_ = new int[size]; }
        A() {}
        A(const A& a) {
            size_ = a.size_;
            data_ = new int[size_];
            cout << "copy " << endl;
        }
        A(A&& a) {                       // 移動(dòng)構(gòu)造函數(shù)
            this->data_ = a.data_;
            a.data_ = nullptr;
            cout << "move " << endl;
        }
    
        ~A() {
            if (data_ != nullptr) {
                delete[] data_;
            }
        }
    
        int* data_;
        int size_;
    };
    
    int main() {
        A a(10);
        A b = a;
        A c = std::move(a);  // 返回右值,調(diào)用移動(dòng)構(gòu)造函數(shù)
        return 0;
    }

    如果不使用 std::move(),會(huì)有很大的拷貝代價(jià),使用移動(dòng)語(yǔ)義可以避免很多無用的拷貝,提供程序性能,c++ 所有的 STL 都實(shí)現(xiàn)了移動(dòng)語(yǔ)義,方便我們使用。

    【注意1】移動(dòng)語(yǔ)義僅針對(duì)于那些實(shí)現(xiàn)了移動(dòng)構(gòu)造函數(shù)的類的對(duì)象,對(duì)于那種基本類型 int、float 等沒有任何優(yōu)化作用,還是會(huì)拷貝,因?yàn)樗鼈儗?shí)現(xiàn)沒有對(duì)應(yīng)的移動(dòng)構(gòu)造函數(shù)。

    【注意2】在實(shí)際開發(fā)中,通常在類中自定義移動(dòng)構(gòu)造函數(shù)的同時(shí),會(huì)再為其自定義一個(gè)適當(dāng)?shù)目截悩?gòu)造函數(shù),由此當(dāng)用戶利用右值初始化類對(duì)象時(shí),會(huì)調(diào)用移動(dòng)構(gòu)造函數(shù);使用左值(非右值)初始化類對(duì)象時(shí),會(huì)調(diào)用拷貝構(gòu)造函數(shù)。

    完美轉(zhuǎn)發(fā)

    首先,解釋一下什么是完美轉(zhuǎn)發(fā),它指的是函數(shù)模板可以將自己的參數(shù)“完美”地轉(zhuǎn)發(fā)給內(nèi)部調(diào)用的其它函數(shù)。所謂完美,即不僅能準(zhǔn)確地轉(zhuǎn)發(fā)參數(shù)的值,還能保證被轉(zhuǎn)發(fā)參數(shù)的左、右值屬性不變。例如:

    template <typename T>
    void function(T t) {
        otherdef(t);
    }

    如上所示,function() 函數(shù)模板中調(diào)用了 otherdef() 函數(shù)。在此基礎(chǔ)上,完美轉(zhuǎn)發(fā)指的是:如果 function() 函數(shù)接收到的參數(shù) t 為左值,那么該函數(shù)傳遞給 otherdef() 的參數(shù) t 也是左值;反之如果 function() 函數(shù)接收到的參數(shù) t 為右值,那么傳遞給 otherdef() 函數(shù)的參數(shù) t 也必須為右值。

    顯然, function() 函數(shù)模板并沒有實(shí)現(xiàn)完美轉(zhuǎn)發(fā)。一方面,參數(shù) t 為非引用類型,這意味著在調(diào)用 function() 函數(shù)時(shí),實(shí)參將值傳遞給形參的過程就需要額外進(jìn)行一次拷貝操作;另一方面,無論調(diào)用 function() 函數(shù)模板時(shí)傳遞給參數(shù) t 的是左值還是右值,對(duì)于函數(shù)內(nèi)部的參數(shù) t 來說,它有自己的名稱,也可以獲取它的存儲(chǔ)地址,因此它永遠(yuǎn)都是左值,也就是說,傳遞給 otherdef() 函數(shù)的參數(shù) t 永遠(yuǎn)都是左值??傊瑹o論從那個(gè)角度看, function() 函數(shù)的定義都不“完美”。

    接下來,那如何實(shí)現(xiàn)完美轉(zhuǎn)發(fā)呢,答案是使用 std::forward():

    • 首先在定義模板函數(shù)時(shí),采用右值引用的語(yǔ)法格式定義參數(shù)類型,由此該函數(shù)既可以接收外界傳入的左值,也可以接收右值;

    • 其次,還需要使用 c++11 標(biāo)準(zhǔn)庫(kù)提供的 std::forword() 模板函數(shù)修飾被調(diào)用函數(shù)中需要維持左、右值屬性的參數(shù)。

    由此即可輕松實(shí)現(xiàn)函數(shù)模板中參數(shù)的完美轉(zhuǎn)發(fā),如下所示:

    void PrintV(int& t) { 
    	cout << "lvalue" << endl; 
    }
    void PrintV(int&& t) { 
    	cout << "rvalue" << endl;
    }
    
    template <typename T>
    void Test(T&& t) {               // 1、采用右值引用的語(yǔ)法格式定義參數(shù)類型
        PrintV(t);
        PrintV(std::forward<T>(t));
        PrintV(std::move(t));
    }
    
    int main() {
        Test(1);                       // lvalue rvalue rvalue
        int a = 1;
        Test(a);                       // lvalue lvalue rvalue
        // 2、使用 std::forword() 模板函數(shù)修飾被調(diào)用函數(shù)
        Test(std::forward<int>(a));    // lvalue rvalue rvalue
        Test(std::forward<int&>(a));   // lvalue lvalue rvalue
        Test(std::forward<int&&>(a));  // lvalue rvalue rvalue
        return 0;
    }
    • Test(1):1是右值,模板中 T &&t 這種為萬能引用,右值 1 傳到 Test 函數(shù)中變成了右值引用,但是調(diào)用 PrintV() 時(shí)候,t 變成了左值,因?yàn)樗兂闪艘粋€(gè)擁有名字的變量,所以打印 lvalue,而 PrintV(std::forward<T>(t)) 時(shí)候,會(huì)進(jìn)行完美轉(zhuǎn)發(fā),按照原來的類型轉(zhuǎn)發(fā),所以打印 rvalue,PrintV(std::move(t)) 毫無疑問會(huì)打印 rvalue。

    • Test(a):a 是左值,模板中 T && 這種為萬能引用,左值 a 傳到 Test 函數(shù)中變成了左值引用,所以有代碼中打印。

    • Test(std::forward<T>(a)):轉(zhuǎn)發(fā)為左值還是右值,依賴于 T,T 是左值那就轉(zhuǎn)發(fā)為左值,T 是右值那就轉(zhuǎn)發(fā)為右值。

    #include <iostream>
    using namespace std;
    
    //重載被調(diào)用函數(shù),查看完美轉(zhuǎn)發(fā)的效果
    void otherdef(int & t) {
        cout << "lvalue\n";
    }
    
    void otherdef(const int & t) {
        cout << "rvalue\n";
    }
    
    //實(shí)現(xiàn)完美轉(zhuǎn)發(fā)的函數(shù)模板
    template <typename T>
    void function(T&& t) {
        otherdef(forward<T>(t));
    }
    
    int main()
    {
        function(5);  // rvalue
        int  x = 1;
        function(x);  // lvalue
        return 0;
    }
    // 打印結(jié)果
    // rvalue  
    // lvalue

    emplace_back 減少內(nèi)存拷貝和移動(dòng)

    對(duì)于STL容器,c++11 后引入了 emplace_back 接口。emplace_back 是就地構(gòu)造,不用構(gòu)造后再次復(fù)制到容器中,因此效率更高??紤]這樣的語(yǔ)句:

    vector<string> testVec;
    testVec.push_back(string(16, 'a'));

    上述語(yǔ)句足夠簡(jiǎn)單易懂,將一個(gè) string 對(duì)象添加到 testVec 中。底層實(shí)現(xiàn):

    • 首先,string(16, &lsquo;a&rsquo;) 會(huì)創(chuàng)建一個(gè) strin g類型的臨時(shí)對(duì)象,這涉及到一次string 構(gòu)造過程。

    • 其次,vector 內(nèi)會(huì)創(chuàng)建一個(gè)新的 string 對(duì)象,這是第二次構(gòu)造。

    • 最后在 push_back 結(jié)束時(shí),最開始的臨時(shí)對(duì)象會(huì)被析構(gòu)。加在一起,這兩行代碼會(huì)涉及到兩次 string 構(gòu)造和一次析構(gòu)。

    c++11 可以用 emplace_back 代替 push_back,emplace_back 可以直接在vector中構(gòu)建一個(gè)對(duì)象,而非創(chuàng)建一個(gè)臨時(shí)對(duì)象,再放進(jìn)vector,再銷毀。emplace_back可以省略一次構(gòu)建和一次析構(gòu),從而達(dá)到優(yōu)化的目的。

    emplace_back 內(nèi)部沒有使用拷貝構(gòu)造函數(shù),也沒有使用移動(dòng)構(gòu)造函數(shù),而是直接調(diào)用構(gòu)造函數(shù),因此更加高效。

    讀到這里,這篇“C++11右值引用和移動(dòng)語(yǔ)義的方法是什么”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識(shí)點(diǎn)還需要大家自己動(dòng)手實(shí)踐使用過才能領(lǐng)會(huì),如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    c++
    AI