C++ 中的 Rule-of-Three

IcedBabyccino發表於2021-06-20

Three 為何物?

所謂的 Three 其實就是 copy constrcutor (複製建構函式)copy assignment operator (複製賦值運算子)destructor (解構函式) 。 在介紹 Rule-of-Three 之前,我們回顧一下 C++ Class 的宣告與定義。

class Empty 
{

}; 

Empty 十分簡單,我沒有為它賦予任何 data members (資料成員) 或顯式地宣告/定義 member functions (成員函式) 。但事實上,編譯器會在必要時為類 Empty 合成必要的成員函式。

int main()
{
    Empty e1 {}; // 建立一個Empty物件
}

由於我沒有為類 Empty 宣告建構函式函式,編譯器自然會為我補充一個 default constructor (預設建構函式)

// C++ 概念程式碼
Empty::Empty() {}

當我們需要透過 e1 去構造更多的相同型別物件的時候,編譯器又幫我們做了以下的事情。編譯器會為類 Empty 新增複製建構函式 Empty::Empty(const Empty&) 和複製賦值運算子 Empty& operator=(const Empty& other)

事實上就類 Empty 的結構而言,編譯器根本不需要生這兩個函式。因為類 Empty 里根本沒有其他的 class object (類物件) 資料成員。編譯器只需要透過 bitwise copy (位逐次複製) 把記憶體逐一複製便能完成任務。不過我們可以假設編譯器會自動生成所需要的函式。

// C++ 概念程式碼
class Empty 
{
public:
    Empty(int val) : _val(val) {} 
private:
    int _val { 0 };
}; 

Empty::Empty(const Empty& other)
{
    _val = other._val;
}

Empty& Empty::operator=(const Empty& other)
{
    _val = other._val;
    return *this;
}

Empty::~Empty()
{

}

// 執行程式碼
int main()
{
    Empty e1 {}; 
    Empty e2 { e1 };
    
    Empty e3;
    e3 = e2; 
}

為什麼要給 Three 定規則?

我們已經對 Three 有了初步的瞭解,同時編譯器可能會在背後做了很多小動作。所以我們不能完全依賴編譯器的行為。

由於類 Empty 新增了新的資料成員,所以我定義了新的建構函式。同時為了避免記憶體洩漏,我也補上了解構函式 Empty::~Empty()

class Empty 
{
public:
    Empty(int val, char c) : _val(val), _cptr(new char(c)) {}
    ~Empty()
    {
        delete _cptr;
    }
    
private:
    int _val { 0 };
    char* _cptr { nullptr };
};

然後我嘗試對類 Empty 的一些物件進行複製操作。此時編譯器再次幫我新增兩個成員函式。

// 概念程式碼
Empty(const Empty& other)
{
    _val = other._val;
    _cptr = other._cptr;
}

Empty& operator=(const Empty& other)
{
    _val = other._val;
    _cptr = other._cptr;
    return *this;
}

// 執行程式碼
int main()
{
    Empty e1 {"1", "empty"};
    Empty e2 { e1 }; 
    
    Empty e3;
    e3 = e2; 
}

編譯器把 e1 的成員逐個複製給 e2 。不過遺憾的是,程式結束之前會崩潰。崩潰原因是 Empty::_cptr 被重複釋放。因為它複製的是 Empty::_cptr 這個指標,而非 Empty::_cptr 這個指標指向的地址存放的值。

int main()
{
    Empty e1(10, 'h');
    auto e2 = e1;
} // creash !!!

所以當 Three 同時存在的時候,為了讓它們都 "安分守己",我們必須給它們頂下規矩,這就是所謂的 Rule of Three

Rules

Three 的問題在於,如果一個類裡面有指標型別 (或者需要手動釋放的資源型別) 的資料成員,編譯器的位逐次複製會讓程式變得不可靠。所以我們必須為讓程式變得安全。基於上面的問題,我們有兩個解決方案。要麼這個類的物件是不允許被複製;要麼這個類的物件允許被複製,但我們必須親自重寫這個類的 Three

方案一:

class Empty
{
public:
    // other user-definfed ctors
    ~Empty()
    {
        delete _cptr;
    }
    
    Empty(const Empty& other) = delete;
    Empty& operator=(const Empty& other) = delete;
    
private:
    // data members
};

方案二:

class Empty
{
public:
    // other user-definfed ctors
     ~Empty()
    {
        delete _cptr;
    }
  
    Empty(const Empty& other)
    {
        _val = other._val;     
        _cptr = new char(*other._cptr);
    }

    Empty& operator=(const Empty& other)
    {
        if (*this == other)
            return *this;
            
        _val = other._val;
        
        if (_cptr != nullptr)
            delete _cptr;

        _cptr = new char(*other._cptr);
    }
    
private:
    // data members
};

啟發

C++ 的物件模型往往不是我們想象中的那麼簡單,編譯器暗地裡會做很多額外的工作。所以我們在管理類物件的資源的時候需要格外小心。