關(guān)于C++ 11移動(dòng)構(gòu)造函數(shù)中的析構(gòu)問題及一些引申

最近在修改用老C++寫的代碼,為了優(yōu)化性能在追加一些和移動(dòng)語義有關(guān)的東西。本來是想要驗(yàn)證在C++ 11中右值在往const引用上綁定的效果,無意間注意到了一個(gè)關(guān)于析構(gòu)的問題。

先上代碼:

#include <iostream>
using namespace std;

auto g_nCounter = 0u;

class TestObj {
private:
    unsigned int m_nCounter = ++g_nCounter;

public:
    TestObj() { cout << "Constructor." << m_nCounter << endl; }
    TestObj(const TestObj& toTarget) { cout << "Copy Constructor." << m_nCounter << endl; }
    TestObj(TestObj&& toTarget) { cout << "Move Constructor." << m_nCounter << endl; }
    ~TestObj() { cout << "Destructor." << m_nCounter << endl; }
};

TestObj GetRef() {
    auto toTemp = TestObj {};
    return toTemp;
}

void TakeRef(const TestObj& obj) {

}

int main(int argc, char** argv) {
    TakeRef(GetRef());
    return 0;
}

驗(yàn)證的內(nèi)容很簡單:GetRef()返回了一個(gè)臨時(shí)的TestObj對(duì)象作為右值,并且作為參數(shù)綁定到了TakeRef()的形參const TestObj& obj上,此時(shí)想要觀察一下整體構(gòu)造的情況。因?yàn)槿绻诶系腃++標(biāo)準(zhǔn)之中,GetRef() 因?yàn)榉祷亓司植孔兞浚虼藢?huì)創(chuàng)建一個(gè)臨時(shí)變量返回,然后綁定給const TestObj& obj,因此這里會(huì)多一次復(fù)制構(gòu)造析構(gòu)的開銷。而在我想法中,在C++ 11之中這里將會(huì)采用新的標(biāo)準(zhǔn)規(guī)定即移動(dòng)構(gòu)造來進(jìn)行綁定,因此運(yùn)行了程序做了驗(yàn)證:

Constructor.1
Move Constructor.2
Destructor.1
Destructor.2

生成toTemp執(zhí)行一次構(gòu)造,沒問題;返回臨時(shí)變量(為方便后面的討論,這里我們命名為_tmp)然后執(zhí)行一次移動(dòng)構(gòu)造,然后綁定到TakeRef的形參,沒問題。

其實(shí)更讓我感到好奇的是析構(gòu)函數(shù)的調(diào)用次序。一般而言,析構(gòu)函數(shù)和構(gòu)造函數(shù)的調(diào)用順序應(yīng)該是顛倒的,比如如果棧上的對(duì)象析構(gòu)函數(shù)調(diào)用順序是c1,c2,c3,那么相應(yīng)地析構(gòu)函數(shù)的調(diào)用順序就是d3,d2,d1。而從實(shí)際的例子上來看,右值引用的引入把這個(gè)規(guī)則稍微改變了。

那么為什么第一個(gè)對(duì)象的析構(gòu)函數(shù)會(huì)事先調(diào)用呢?原因在于在_tmp在執(zhí)行拷貝構(gòu)造函數(shù)完畢的時(shí)候,編譯器就讓拷貝的toTemp析構(gòu)了。

這里不妨回憶一下右值引用(或者說move語義)引入的最大用處:事先語義層面的資源轉(zhuǎn)移,避免拷貝。

在很多介紹右值引用的文章中,都強(qiáng)調(diào)了轉(zhuǎn)移這個(gè)概念,并且告訴我們?nèi)绻麑?duì)象b通過move語義將資源轉(zhuǎn)移給了a,那么b就是個(gè)死對(duì)象,但是這種描述并沒有具體到編譯器的實(shí)現(xiàn)層面上:怎么個(gè)死法

于是這里告訴我們?cè)趺磦€(gè)死法了:被轉(zhuǎn)移的那個(gè)對(duì)象將會(huì)在移動(dòng)構(gòu)造結(jié)束的時(shí)候自動(dòng)被釋放掉。

于是有了上面的那個(gè)結(jié)果。

因此進(jìn)一步地,我覺得在這里可能有個(gè)容易被誤解的地方需要被澄清:移動(dòng)語義移動(dòng)的是資源而不是對(duì)象本身。這里首先要弄清楚

資源到底是什么?

對(duì)于我們編寫的代碼而言,資源就是一個(gè)對(duì)象內(nèi)部的那些成員變量,比如一個(gè)簡單實(shí)現(xiàn)的vector的資源可能就是內(nèi)部的一個(gè)數(shù)組,而當(dāng)我們想要轉(zhuǎn)移資源的時(shí)候,轉(zhuǎn)移的其實(shí)就是這個(gè)數(shù)組,而不是對(duì)象本身。

因此實(shí)際上在進(jìn)行資源移動(dòng)的時(shí)候,仍然是新建了對(duì)象的,只是在新建對(duì)象之后我們并沒有拷貝資源,而是直接轉(zhuǎn)移了資源。換言之,整個(gè)過程只是把過去的拷貝構(gòu)造函數(shù)的調(diào)用改成了移動(dòng)構(gòu)造函數(shù)的調(diào)用,最后加上一個(gè)釋放掉被轉(zhuǎn)移資源的原始對(duì)象本身的操作。

因此,回到上面那個(gè)vector的例子,我們可能寫出如下代碼:

MyVector(MyVector&& orgVector) {
    if(this != &orgVector) {
        this->m_pArray = orgVector.m_pArray; // 轉(zhuǎn)移資源
        orgVector.m_pArray = nullptr;
    }
}

~MyVector() {
    if(!m_pArray) {
        delete[] m_pArray; 
    }
}

而在這個(gè)轉(zhuǎn)移構(gòu)造函數(shù)結(jié)束之后,orgVector將會(huì)被自動(dòng)析構(gòu),這樣子就能夠保證資源轉(zhuǎn)移的安全性。但同時(shí),這個(gè)安全仍然需要我們來保證,具體反映在兩點(diǎn):1、構(gòu)造函數(shù)中我們轉(zhuǎn)移m_pArray之后,我們將其設(shè)置為nullptr;2、在析構(gòu)函數(shù)中,首先判斷m_pArray是否為nullptr,如果是,就不能進(jìn)行釋放。為什么呢?假設(shè)我們?cè)跇?gòu)造函數(shù)中轉(zhuǎn)移m_pArray之后不去把它設(shè)置為nullptr,那么當(dāng)移動(dòng)構(gòu)造函數(shù)執(zhí)行完畢之后, orgVector將會(huì)被自動(dòng)析構(gòu),然后調(diào)用析構(gòu)函數(shù),這個(gè)時(shí)候就出問題了:此時(shí)析構(gòu)函數(shù)會(huì)把m_pArray指向的堆內(nèi)存直接釋放掉,接盤俠——新的那個(gè)MyVector對(duì)象還活不活了?

那么能不能使用拷貝構(gòu)造來完成呢?

答案是可以,同時(shí)也不可以。

可以強(qiáng)行用以下方法來完成:

MyVector(const MyVector& orgVector) {
    if(this != &orgVector) {
        this->m_pArray = orgVector.m_pArray; // 轉(zhuǎn)移資源
        const_cast<MyVector&>(orgVector).m_pArray = nullptr;
    }
}

但是這樣做造成了兩個(gè)問題:

  1. 拷貝構(gòu)造函數(shù)原本只是為了“拷貝”,本不應(yīng)該去修改被拷貝對(duì)象的任何成員內(nèi)容的,然而我們這里使用了const_cast強(qiáng)行讓它能夠修改,無疑是破壞了這條規(guī)則;
  2. 拷貝構(gòu)造函數(shù)完成后不會(huì)自動(dòng)釋放原來的那個(gè)對(duì)象,這個(gè)和轉(zhuǎn)移資源的語義就不符合了。

因此,不能也不應(yīng)該這樣子做。

因此通過上面的討論,我們得出了這樣子的結(jié)論:

  1. 移動(dòng)語義下,仍然創(chuàng)建了新的對(duì)象;
  2. 之所以說移動(dòng)構(gòu)造的開銷沒有復(fù)制構(gòu)造的開銷大,是因?yàn)樵谝苿?dòng)構(gòu)造之中完成的是資源轉(zhuǎn)移而不是拷貝——當(dāng)然,這個(gè)也需要編寫代碼的人員來保證;
  3. 移動(dòng)構(gòu)造函數(shù)結(jié)束后,被移動(dòng)的那個(gè)對(duì)象會(huì)被自動(dòng)釋放。

此外補(bǔ)充一個(gè)關(guān)于C++ 11中各個(gè)構(gòu)造函數(shù)的default/delete的討論:

why does deleting move constructor cause vector to stop working

其中我覺得最重要的兩點(diǎn)在于:

  1. C++ 11中,如果手動(dòng)設(shè)置移動(dòng)構(gòu)造函數(shù)為delete,那么編譯器將會(huì)自動(dòng)地把復(fù)制構(gòu)造函數(shù)與operator=都給delete了;
  2. C++ 11中,如果手動(dòng)編寫了移動(dòng)構(gòu)造函數(shù)的話,那么編譯器將會(huì)默認(rèn)將復(fù)制構(gòu)造函數(shù)設(shè)置為delete

關(guān)于右值引用的更多話題,比如完美轉(zhuǎn)發(fā)將會(huì)放在后面來介紹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容