深入理解C++11 3.3 右值引用:移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)

首先,本章很長(zhǎng),也較難理解,建議讀者有大段連續(xù)的時(shí)間看這個(gè)。。。

3.3.1 指針成員與拷貝構(gòu)造

關(guān)于拷貝構(gòu)造函數(shù)的調(diào)用時(shí)間,可以看這篇文章
如果類(lèi)中包含了指針,需要小心處理,下面是一段有問(wèn)題的代碼

class C {
public:
    C():i(new int(0)){
        cout << "none argument constructor called" << endl;
    }
    
    ~C(){
       cout << "destructor called" << endl;
        delete i;
    }
    
    int* i;
};

int main(){
    C c1;
    C c2 = c1;
    
    cout << *c1.i << endl;
    cout << *c2.i << endl;
    
    return 0;
}

XCode代碼執(zhí)行輸出

none argument constructor called
0
0
destructor called
destructor called
CppTest(50956,0x1000ad5c0) malloc: *** error for object 0x10070c510: pointer being freed was not allocated
CppTest(50956,0x1000ad5c0) malloc: *** set a breakpoint in malloc_error_break to debug

原因是編譯期會(huì)默認(rèn)為類(lèi)創(chuàng)建拷貝構(gòu)造函數(shù),而默認(rèn)的拷貝構(gòu)造函數(shù)只是簡(jiǎn)單的賦值,對(duì)類(lèi)C,系統(tǒng)默認(rèn)生成的拷貝構(gòu)造函數(shù)如

C(const C& c):i(c.i){
}

導(dǎo)致c1和c2的i值一樣,即指向同一片地址,當(dāng)c1析構(gòu)之后,c2.i就成為了一個(gè)“懸掛指針”(dangling pointer),不再指向有效的內(nèi)存了,如果對(duì)懸掛指針再次進(jìn)行delete就會(huì)出現(xiàn)嚴(yán)重的錯(cuò)誤。
以上系統(tǒng)生成的默認(rèn)拷貝構(gòu)造函數(shù)做的是淺拷貝(shallow copy),為了解決這個(gè)問(wèn)題,通常是用戶自定義拷貝構(gòu)造函數(shù)實(shí)現(xiàn)深拷貝(deep copy),修正如下

class C {
public:
    C():i(new int(0)){
        cout << "none argument constructor called" << endl;
    }
    
    //增加此拷貝構(gòu)造函數(shù),根據(jù)傳入的c,new一個(gè)新的int給i變量
    C(const C& c) :i(new int(*c.i)){
        
    }
    
    ~C(){
       cout << "destructor called" << endl;
        delete i;
    }
    
    int* i;
};

執(zhí)行代碼后如下

none argument constructor called
0
0
destructor called
destructor called
Program ended with exit code: 0

3.3.2 移動(dòng)語(yǔ)義

拷貝函數(shù)中為指針成員分配新的內(nèi)存再進(jìn)行內(nèi)容拷貝的方法在C++中幾乎被視為不可違背的,不過(guò)有些時(shí)候卻是不必要的。如下代碼:

//這是一個(gè)成員包含指針的類(lèi)
class HasPtrMem {
public:
    HasPtrMem() : d(new int(0)) {
        cout << "Construct:" << ++n_cstr << endl;
    }
    
    HasPtrMem(const HasPtrMem& h) {
        cout << "Copy construct:" << ++n_cptr << endl;
    }
    
    ~HasPtrMem() {
        cout << "Destruct:" << ++n_dstr << endl;
    }
    
private:
    int* d;
    static int n_cstr;
    static int n_dstr;
    static int n_cptr;
};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;

HasPtrMem GetTemp() {
    return HasPtrMem();//①
}

int main(){
    HasPtrMem m = GetTemp();//②
    
    return 0;
}

這里我沒(méi)用Xcode編譯運(yùn)行,因?yàn)樵贐uild Setting里增加-fno-elide-constructors編譯器依然還是優(yōu)化了,所以根據(jù)教材用命令行執(zhí)行

g++ -std=c++11 main.cpp -fno-elide-constructors

會(huì)在cpp文件下生成一個(gè)a.out文件,在命令行執(zhí)行./a.out輸出

Construct:1
Copy construct:1
Destruct:1
Copy construct:2
Destruct:2
Destruct:3

構(gòu)造函數(shù)被調(diào)用1次,是在①處,第一次調(diào)用拷貝構(gòu)造函數(shù)是在GetTemp return的時(shí)候,將①生成的變量拷貝構(gòu)造出一個(gè)臨時(shí)值,來(lái)當(dāng)做GetTemp的返回,第二次拷貝構(gòu)造函數(shù)是在②處。同時(shí)就有了于此對(duì)應(yīng)的三次析構(gòu)函數(shù)的調(diào)用。例子里用的是一個(gè)int類(lèi)型的指針,而如果該指針指向的是非常大的堆內(nèi)存數(shù)據(jù)的話,那沒(méi)拷貝過(guò)程就會(huì)非常耗時(shí),而且由于整個(gè)行為是透明且正確的,分析問(wèn)題時(shí)也不易察覺(jué)。

在C++中,我們可以通過(guò)移動(dòng)構(gòu)造函數(shù)解決此問(wèn)題,修改代碼如下:

//這是一個(gè)成員包含指針的類(lèi)
class HasPtrMem {
public:
    HasPtrMem() : d(new int(0)) {
        cout << "Construct:" << ++n_cstr << endl;
    }
    
    HasPtrMem(const HasPtrMem& h) {
        cout << "Copy construct:" << ++n_cptr << endl;
    }
    
    HasPtrMem(HasPtrMem&& h):d(h.d) {
        h.d = nullptr; //③注意對(duì)之前的h賦空指針
        cout << "Move construct:" << ++n_mvtr << endl;
    }
    
    ~HasPtrMem() {
        cout << "Destruct:" << ++n_dstr << endl;
    }
    
private:
    int* d;
    static int n_cstr;
    static int n_dstr;
    static int n_cptr;
    static int n_mvtr;
};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;

HasPtrMem GetTemp() {
    return HasPtrMem();
}

int main(){
    HasPtrMem m = GetTemp();
    
    return 0;
}

輸出

Construct:1
Move construct:1
Destruct:1
Move construct:2
Destruct:2
Destruct:3

這里通過(guò)指針賦值的方式,將d的內(nèi)存直接偷了過(guò)來(lái),避免了拷貝構(gòu)造函數(shù)的調(diào)用。注意③,這里需要對(duì)原來(lái)的d進(jìn)行賦空值,因?yàn)樵谝苿?dòng)構(gòu)造函數(shù)完成之后,臨時(shí)對(duì)象會(huì)立即被析構(gòu),如果不改變d,那臨時(shí)對(duì)象被析構(gòu)時(shí),因?yàn)橥祦?lái)的d和原本的d指向同一塊內(nèi)存,會(huì)被釋放,成為懸掛指針,會(huì)造成錯(cuò)誤。

為什么不用函數(shù)參數(shù)里帶個(gè)指針或者引用當(dāng)返回結(jié)果呢?不是性能的問(wèn)題,而是代碼編寫(xiě)效率及可讀性不好,如:

string *a;
int c = 1
int &b = c;
Calculate(GetTemp(),b);//最后一個(gè)參數(shù)用于返回結(jié)果

最后說(shuō)明一下移動(dòng)構(gòu)造函數(shù)被調(diào)用的時(shí)機(jī):一旦用到的是臨時(shí)變量,那么移動(dòng)語(yǔ)義就可以得到執(zhí)行。下一節(jié)講下C++的值是如何分類(lèi)的。未完待續(xù),后面還有4節(jié)。。。

3.3.3 左值、右值與右值引用

關(guān)于左值(lvalue)和右值(rvalue)的判別方法:

  • 在賦值表達(dá)式中,出現(xiàn)在等號(hào)左邊的是“左值”,等號(hào)右邊的是“右值”,如a = b + c;中,a是左值,而b+c是右值;
  • 可以取地址的、有名字的是左值,反之是右值,對(duì)于a = b + c;&a是允許的操作,&(b+c)是不允許的操作,所以a是左值,b+c是右值。

而在C++11中右值是由兩個(gè)概念構(gòu)成的,一個(gè)是將亡值(xvalue, eXpriring Value),另個(gè)一個(gè)則是純右值(prvalue, Pure Rvalue)。
其中純右值包括:

  • 非引用返回的函數(shù)返回的臨時(shí)變量值
  • 運(yùn)算表達(dá)式,如1+3產(chǎn)生的臨時(shí)變量值
  • 不跟對(duì)象關(guān)聯(lián)的字面量,如2、’c‘、true
  • 類(lèi)型轉(zhuǎn)換函數(shù)的返回值
  • lamda表達(dá)式

將亡值賊是C++11新增的跟右值引用相關(guān)的表達(dá)式,包括:

  • 返回右值引用T&&的函數(shù)返回值
  • std::move的返回值
  • 轉(zhuǎn)換為T(mén)&&的類(lèi)型轉(zhuǎn)換函數(shù)的返回值

而剩余的,可以標(biāo)識(shí)函數(shù)、對(duì)象的值都屬于左值。在C++11的程序中,所有的值必屬于左值、將亡值、純右值三者之一。

在C++11中,右值引用就是對(duì)一個(gè)右值進(jìn)行引用的類(lèi)型。由于右值不具有名字,我們也只能通過(guò)引用的方式找到它的存在。通常我們只能是從右值表達(dá)式獲得其引用。比如:

T&& a = ReturnRvalue();①

右值引用和左值引用都是引用類(lèi)型,都必須立即進(jìn)行初始化。引用類(lèi)型本身并不擁有綁定對(duì)象的內(nèi)存,只是該對(duì)象的一個(gè)別名。左值引用是具名變量值的別名,右值引用則是匿名變量的別名。

在上面①的例子中,ReturnRvalue函數(shù)返回的右值在表達(dá)式語(yǔ)句結(jié)束后,其生命也就終結(jié)了,而通過(guò)右值引用的聲明,該右值又“重獲新生”,其生命期將于右值引用類(lèi)型a的生命期一樣。只要a還“活著”,該右值臨時(shí)量將會(huì)一直“存活”下去。
所以相比于一下語(yǔ)句:

T b = ReturnRvalue();

①的聲明方式會(huì)少一次對(duì)象的析構(gòu)和一次對(duì)象構(gòu)造。因?yàn)閍是右值引用,直接綁定了ReturnRvalue()返回的臨時(shí)量,而b是由臨時(shí)值構(gòu)造的,而臨時(shí)量在表達(dá)式結(jié)束后會(huì)析構(gòu)因而會(huì)多一次析構(gòu)和構(gòu)造的開(kāi)銷(xiāo)。
注意,能夠聲明右值引用a的前提是ReturnRvalue返回的是一個(gè)右值。通常右值引用是不能夠綁定到任何左值的,如下代碼會(huì)導(dǎo)致編譯無(wú)法通過(guò):

int c;
int &&d = c;

有的時(shí)候,我們可能不知道一個(gè)類(lèi)型是否是引用類(lèi)型,以及是左值引用還是右值引用。標(biāo)準(zhǔn)庫(kù)<type_traits>頭文件中提供了3個(gè)類(lèi)模板:is_rvalue_reference、is_lvalue_reference和is_reference,比如:

cout << is_rvalue_reference<string &&>::value;

3.3.4 std::move 強(qiáng)制轉(zhuǎn)化為右值

C++11中,<utility>中提供了函數(shù)std::move,功能是將一個(gè)左值強(qiáng)制轉(zhuǎn)化為右值引用,繼而我們可以通過(guò)右值引用使用該值,用于移動(dòng)語(yǔ)義。std::move基本等同于一個(gè)類(lèi)型轉(zhuǎn)換:

static_cast<T&&>(lvalue);

被轉(zhuǎn)化的左值,其生命期并沒(méi)有隨著左右值的轉(zhuǎn)化而改變。下面是一個(gè)正確使用std::move的例子

class HugeMem {
public:
    HugeMem(int size): sz(size>0 ? size: 1) {
        c = new int[size];
    }
    
    ~HugeMem() {
        delete [] c;
    }
    
    HugeMem(HugeMem&& h) : sz(h.sz), c(h.c) {
        h.c = nullptr;
    }
    
    int* c;
    int sz;
};

class Moveable {
public:
    Moveable(): i(new int[3]), h(1024) {}
    
    ~Moveable() {
        delete [] i;     
    }
    
    Moveable(Moveable&& m) : i(m.i), h(move(m.h)) { //使用move將m.h轉(zhuǎn)為右值引用,繼而調(diào)用HugeMem的移動(dòng)構(gòu)造函數(shù)
        m.i = nullptr;
    }
    
    int *i;
    HugeMem h;
};

Moveable getTemp() {
    Moveable tmp = Moveable();
    cout << hex << "Huge Mem from " << __func__ << "@" << tmp.h.c << endl;
    return tmp;
}

int main(){
    Moveable a(getTemp());//因?yàn)間etTemp()返回的是右值,所以會(huì)調(diào)用Moveable的移動(dòng)構(gòu)造函數(shù)
    cout << hex << "Huge Mem from " << __func__ << "@" << a.h.c << endl;
    return 0;
}

輸出

Huge Mem from getTemp@0x104002000
Huge Mem from main@0x104002000

需要注意的是,在編寫(xiě)移動(dòng)構(gòu)造函數(shù)的時(shí)候,應(yīng)該總是使用std::move轉(zhuǎn)換擁有形如堆內(nèi)存、文件句柄的等資源的成員為右值,這樣一來(lái),如果成員支持移動(dòng)構(gòu)造的話,就可以實(shí)現(xiàn)其移動(dòng)語(yǔ)義,即使成員沒(méi)有移動(dòng)構(gòu)造函數(shù),也會(huì)調(diào)用拷貝構(gòu)造,因?yàn)椴粫?huì)引起大的問(wèn)題。

3.3.5 移動(dòng)語(yǔ)義的一些其他問(wèn)題

移動(dòng)語(yǔ)義一定是要改變臨時(shí)變量的值(這里有以為,需要解決,目前沒(méi)看出哪里一定要改變,先這么硬背吧)。如聲明:

Moveable(const Moveale &&);//這個(gè)對(duì)應(yīng)3.3.4的例子,如果這樣聲明移動(dòng)構(gòu)造函數(shù)會(huì)報(bào)錯(cuò)
image.png

而如果是將3.3.4的例子中的Moveable getTemp()改為const Moveable getTemp(),再執(zhí)行命令

g++ -std=c++11 main.cpp -fno-elide-constructors

注意上面的改動(dòng)在Xcode中是可以運(yùn)行的,可以正確調(diào)用到移動(dòng)構(gòu)造函數(shù),但是通過(guò)命令行會(huì)提示

copy constructor is implicitly deleted because 'Moveable' has a user-declared move constructor

可見(jiàn)Moveable a(getTemp());實(shí)際是要調(diào)用Moveable的拷貝構(gòu)造函數(shù)。報(bào)錯(cuò)原因顯示聲明了移動(dòng)構(gòu)造函數(shù),編譯器就不會(huì)為類(lèi)生成默認(rèn)的拷貝構(gòu)造函數(shù)了,所以提示沒(méi)有顯示聲明拷貝構(gòu)造函數(shù)。

在C++11中,拷貝/移動(dòng)改造函數(shù)有以下3個(gè)版本:

  • T Object(T&)
  • T Object(const T&)
  • T Object(T&&)

其中常量左值引用的版本是一個(gè)拷貝構(gòu)造函數(shù)版本,右值引用參數(shù)的是一個(gè)移動(dòng)構(gòu)造函數(shù)版本。默認(rèn)情況下,編譯器會(huì)為程序員隱式地生成一個(gè)移動(dòng)構(gòu)造函數(shù),但是如果聲明了一自定義的拷貝構(gòu)造函數(shù)、拷貝賦值函數(shù)、移動(dòng)構(gòu)造函數(shù)、析構(gòu)函數(shù)中的一個(gè)或者多個(gè),編譯器都不會(huì)再生成默認(rèn)版本。所以在C++11中,拷貝構(gòu)造函數(shù)、拷貝賦值函數(shù)、移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值函數(shù)必須同時(shí)提供,或者同時(shí)不提供,只聲明其中一種的話,類(lèi)都僅能實(shí)現(xiàn)一種語(yǔ)義。

只實(shí)現(xiàn)一種語(yǔ)義在類(lèi)的編寫(xiě)中也是非常常見(jiàn)的,比如如果只實(shí)現(xiàn)移動(dòng)語(yǔ)義,則表明該類(lèi)型的變量擁有的資源只能被移動(dòng),不能被拷貝,那么這樣的資源必須是唯一的,如智能指針、文件流。

在<type_traits>里,可以通過(guò)一些輔助的模板類(lèi)來(lái)判斷一個(gè)類(lèi)型是否是可以移動(dòng)的,如:

  • is_move_constructible
  • is_trivially_move_constructible
  • is_nothrow_move_constructible

使用方法都是使用value成員,如

cout << is_move_constructible<UnknowTYpe>::value;

有了移動(dòng)語(yǔ)義,可以實(shí)現(xiàn)高性能的置換函數(shù),如:

template <class T>
void swap(T& a, T& b) {
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

如果T是可以移動(dòng)的,則不會(huì)有資源的釋放和申請(qǐng),如果T不可移動(dòng)但是可以拷貝,則和普通聲明一樣了。

要注意的是,盡量不要編寫(xiě)會(huì)拋出異常的移動(dòng)構(gòu)造函數(shù),因?yàn)橛锌赡芤苿?dòng)沒(méi)完成,會(huì)導(dǎo)致一些指針成為懸掛指針,通過(guò)添加noexcept關(guān)鍵字,可以保證移動(dòng)構(gòu)造函數(shù)拋出異常直接終止程序。

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

完美轉(zhuǎn)發(fā)(perfect forwarding),是指在模板函數(shù)中,完全依照模板的參數(shù)類(lèi)型講參數(shù)傳遞給模板中調(diào)用的另外一個(gè)函數(shù),如:

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

這是一個(gè)參數(shù)透?jìng)鞯膶?shí)現(xiàn),但是因?yàn)槭褂米罨绢?lèi)型轉(zhuǎn)發(fā),會(huì)在傳參的時(shí)候產(chǎn)生一次額外的臨時(shí)對(duì)象拷貝,因?yàn)橹荒苷f(shuō)是轉(zhuǎn)發(fā),但不完美。所以通常需要的是一個(gè)引用類(lèi)型餐護(hù)士,不會(huì)有拷貝的開(kāi)銷(xiāo)。其次需要考慮函數(shù)對(duì)類(lèi)型的接受能力,因?yàn)槟繕?biāo)函數(shù)可能需要既接受左值引用,又接受右值引用,如果轉(zhuǎn)發(fā)函數(shù)只能接受其中的一部分,也不完美。

對(duì)應(yīng)代碼

typedef const A T;
typedef T& TR;
TR& v = 1;

在C++11中引入了一條所謂“引用折疊”的新語(yǔ)言規(guī)則,規(guī)則如下

TR的類(lèi)型定義 聲明v的類(lèi)型 v的實(shí)際類(lèi)型
T& TR A&
T& TR& A&
T& TR&& A&
T&& TR A&&
T&& TR& A&
T&& TR&& A&&

規(guī)則就是一單定義中出現(xiàn)了左值引用,引用折疊總是優(yōu)先將其折疊為左值引用。前三行TR定義為T(mén)&,則v世界類(lèi)型為A&,第五行的v的類(lèi)型為T(mén)R&,則v的實(shí)際類(lèi)型也為A&,其他則為右值引用。于是我們把轉(zhuǎn)發(fā)函數(shù)改為:

template <typename T>
void IamForwarding(T&& t) {
    IrunCodeActually(static_cast<T&&>(t));
}

對(duì)于傳入的左值引用

void IamForwarding(X& && t) {
    IrunCodeActually(static_cast<X& &&>(t));
}

折疊后是

void IamForwarding(X& t) {
    IrunCodeActually(static_cast<X&>(t));
}

對(duì)于右值引用

void IamForwarding(X&& && t) {
    IrunCodeActually(static_cast<X&& &&>(t));
}

折疊后是

void IamForwarding(X&& t) {
    IrunCodeActually(static_cast<X&&>(t));
}

此處的static_cast類(lèi)似std::move的作用,將左值轉(zhuǎn)換為右值引用。不過(guò)在C++11中,用于完美轉(zhuǎn)發(fā)的函數(shù)不叫move,叫forward,所以也可以這么寫(xiě)

void IamForwarding(X&& t) {
    IrunCodeActually(forward(t));
}

move和forward實(shí)現(xiàn)差別不大,但是為了不同用途,有了不同命名。
下面是完美轉(zhuǎn)發(fā)的例子:

void run(int && m) { cout << "rvalue ref" << endl; }
void run(int & m) { cout << "lvalue ref" << endl; }
void run(const int && m) { cout << "const rvalue ref" << endl; }
void run(const int & m) { cout << "const lvalue ref" << endl; }

template <typename T>
void perfectForward(T&& t) {
    run(forward<T>(t));
}

int main(){
    int a;
    int b;
    const int c = 1;
    const int d = 0;
    
    perfectForward(a);
    perfectForward(move(b));
    perfectForward(c);
    perfectForward(move(d));
    
    return 0;
}

輸出

lvalue ref
rvalue ref
const lvalue ref
const rvalue ref
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容