close
二、Constructors, Destructors and Assignment Operators
幾乎每個 classes 都會有一個或多個建構式、一個解構式及一個 copy assignment operator,學著控制這些基礎操作,像是產出新物件並確保它被初始化、擺脫舊物件並確保它被適當處理及賦與物件新值,如果這些函式犯錯,會導致深遠且令人不愉快的後果,且將遍及整個 classes,本章的引導,將可把這些函式良好地集結在一起,形成 classes 的脊柱。
05、Know what functions C++ silently writes and calls.
什麼時候 empty class (空類別) 將不再是個 empty class 呢?當 C++ 處理過它之後。
是的,如果 class 本身沒有宣告,編譯器就會為它宣告一個 copy constructor、一個 copy assignment operator 和一個 destructor,如果沒有宣告任何的 constructor,則編譯器也會再宣告一個 default constructor,而所有的這些函式都是 public 且 inline 的,詳見條款 30。
因此,如果寫下:
效果等同於:
不過也唯有這些函式被需要時(被呼叫),它們才會被編譯器產生出來。
編譯器為何需要自動產生這些函式?
default constructor 及 destructor 主要是給編譯器一個地方用來放置「藏身幕後」的程式碼,像是喚起 base class 和 non-static 成員變數的 constructor 及 destructor。
如果使用者自行宣告了一個 constructor,則編譯器將不再為它產生 default constructor。
一般來說,編譯器所產生的 destructor 是 non-virtual (見條款7),除非這個 class 的 base class 本身有宣告 virtual destructor,這種情況下,這個函式的虛擬屬性(virtualness) 主要來自 base class。
至於 copy constructor 及 copy assignment operator,編譯器產生的版本只是單純地將來源物件的每個 non-static 成員變數拷貝到目標物件。
NamedObject,由於已經宣告 constructor,所以編譯器將不再為它產生 default constructor。
但也由於沒有宣告 copy constructor 及 copy assignment operator,所以如果這些函數有被呼叫的話,編譯器將會自動為它產生。
編譯器生成的 copy constructor 必須以 no1.nameValue 及 no1.objectValue 為初值,來設定 no2.nameValue 及 no2.objectValue。
其中 nameValue 的型別是 string,所以 no2.nameValue 初始化的方式是喚起 string 的 copy constructor 並以 no1.nameValue 為引數。
另一個成員 objectValue 的型別是 int,是一個內建型別,所以 no2.objectValue 會以「拷貝 no1.objectValue 內的每個 bits」來完成初始化。
編譯器為 NamedObject 所產生的 copy assignment operator ,其行為基本上和 copy constructor 如出一轍,但一般而言,只有在所生出的程式碼合法,具有適當機會證明它有意義的情況下,編譯器才會自動產生如前述的 copy assignment operator,否則編譯器將拒絕為 class 生出 operator =。
對上述例子,作些修改:
考慮下列操作,將會發生什麼事:
由於 C++ 並不允許「讓 reference 改指向不同物件」,也由於修改 p.nameValue 所指的那個 string ,會進而影響「持有 pointers 或 references 指向該 string 」的其它物件,所以 C++ 對於這之類的 copy assignment operator 的回應是拒絕編譯那一行賦值動作,此時,就需要自行定義 copy assignment operator。
而面對 「內含 const 成員」的 classes,編譯器的反應也是相同,因為更改 const 成員是不合法的。
還有一種情況,編譯器也會拒絕為其產生 copy assignment operator:
如果某個 base classes 將 copy assignment operator 宣告為 private,則編譯器將拒絕為其 derived classes 生成 copy assignment operator。
編譯器可以暗自為 class 產生: default constructor、copy constructor、copy assignment operator 及 destructor。
06、Explicitly disallow the use of compiler-generated functions you do not want.
通常如果不希望 class 支援某些特定機能,只要不宣告相對應的函式即可,不過這個策略,對於 copy constructor 及 copy assignment operator 卻不起作用,因為如條款5 所提:
如果沒有宣告它們,而在它們被呼叫時,編譯器將會自動產生這些函式。
而到底該如何作,才能阻止 class 支援 copying 呢?
答案的關鍵是:預防 copy constructor 及 copy assignment operator 被自動產生出來。
作法是:將 copy consctuctor 及 copy assignment operator 宣告為 private。
如此,將阻止了編譯器會暗自產生其專屬版本的函式。
將 copy constructor 及 copy assignment operator 宣告為 private 之後,為了預防 member function 及 friend class 的呼叫,這兩個 function 將不實作出來,如此,如果不慎呼叫了任何一個檔案,會獲得一個連結錯誤(linkage error)。
「將成員函式宣告為 private,而且故意不實作它們」這一技倆是廣被使用的,在標準程式庫實作碼中的 ios_base, basic_ios, entry ,無論是哪一個,其 copy constructor 和 copy assignment operator ,也都被宣告為 private ,而且也都沒定義。
有了上述 class 的定義,如果不慎在 member function 或 friend function 企圖拷貝 HomeForSale,則連結器會發出抱怨,如果是其它 function ,則編譯器會進行阻止。
不過有沒有辦法讓錯誤在編譯期時就被發現?
答案是有的,但要依靠一個專門為阻止 copying 動作而設計的 base class:
而為了阻止 HomeForSale 物件被拷貝,唯一需要做的就是繼承 Uncopyable:
原因在於,只要有任何嘗試要拷貝 HomeForSame 物件,則編譯器便試著產生一個 copy constructor 和一個 copy assignment operator,但如條款 12 所說,這些「編譯器生成版」函式會嘗試呼叫 base class 的對應兄弟,但那些呼叫會被編譯器拒絕,因為 base class 的拷貝函式是 private。
為駁回編譯器自動提供的機能,可將相應的成員函式宣告為 private 並且不予實作,又或是使用像 Uncopyable 這樣的 base class 也是一種作法。
07、Declare destructors virtual in polymorphic base classes.
有很多種方式可以記錄時間,因此設計一個 TimeKeeper base class 和一些 derived classes 作為不同的計時方式,是相當合理的:
有時在指標的設計上為了方便,會設計一個 factory function,factory function 會「傳回一個指向新生成之 derived class 物件的 base class 指標」:
而呼叫的方式為:
此時,先不管條款 13 說「依賴客戶執行 delete 動作,基本上便帶有某種錯誤傾向」,也不管條款 18 談到 factory function 介面該如何修改以預防常見之客戶錯誤。
上述程式碼有一個更根本的缺點,即使把每樣事情都做對了,仍沒辦法知道程式如何行動。
問題點是 getTimeKeeper() 所回傳的是 derived class 物件,而這個物件被 base class 指標指的,而且被 delete,然而在 base class 卻有一個 non-virtual destructor 。
C++ 明白指出,當 derived class 經由一個 base class 指標被刪除,而該 base class 帶著一個 non-virtual destructor ,這個結果是未有定義的,而實際上的情況是:物件的 derived 成分沒被 destruct,base class 成分通常會被 destruct。
消除這個問題的方法是:給 base class 一個 virtual destructor,此後 delete derived class 物件就會 destruct 整個物件:
像 TimeKeeper 這類的 base class,除了 destructor 之外,通常還有其它 virtual function,因為 virtual function 的目的是允許 derived classes 的實作可以客製作,詳見條款 34。
而任何 class 只要帶有 virutal function,也幾乎確定應該也要有一個 virtual destructor。
但如果無端將 destructor 宣告為 virtual ,往往是個餿主意,因為要實作出 virtual function,物件必攜帶某些資訊,主要用來決定在執行期哪一個 virtual funcion 該被喚起,這份資料由一個 vptr 的指標指出,vptr 指向一個由函式指標構成的陣列,稱為 vtbl,每個帶有 virtual function 的 class 都有一個相應的 vtable,這將增加物件的體積,也會導致無法將物件傳遞至其它語言所寫的函式。。
即使 class 完全不帶 virtual function,也是會有用法錯誤的情況:
相同的分析適用於所有不帶 virtual destructor 的 class,包括所有 STL 容器,如:vector, list, set, tr1::unordered_map,所以,如果想繼承一個標準容器或其它「帶有 non-virtual destructor」的 class,拒絕誘惑吧!
有時候,會希望某個 class 是 abstract class ,但卻沒有任何的 pure virtual function,此時,可以為這個 class 宣告一個 pure virtual destructor:
這個 class 有個 pure virtual 函式,所以是一個 abstract class,同時它有個 virtual destructor ,所以不必擔心 destructor 的問題。
然而,卻必須為 pure virtual destructor 提供一份定義:
destructor 的運作方式是: the most derived class 會最先被解構,然後是每個 base class 的 destructor 被呼叫,而編譯器在 AWOV 的 derived class 的 destructor 中,會產生一個對 ~AWOV() 的呼叫,所以必需為這個函式提供定義,否則連結器會發出抱怨。
「給 base classes 一個 virtual destructor」,這個規則只適用於 polymorphic base classes 身上,其設計目的是為了用來「透過 base class 介面處理 derived class 物件」。
polymorphic base classes 應該宣告一個 virtual destructor,如果 class 帶有任何 virtual function,也應該擁有一個 virtual destructor。
Classes 的設計目的如果不是為了作為 base class,或不是具備 polymorphically,就不應該宣告 virtual destructor。
A class that declares or inherits a virtual function is called a polymorphic class.
08、Prevent exceptions from leaving destructors.
C++ 雖然不禁止 destructor 吐出 exception,但這樣子的作法並不被鼓勵,考慮以下的程式碼:
當 vector v 被 destruct 時,它有責任將其內含的所有 Widgets 全部 destruct,假設含有 10 個 widgets,如果在 destruct 第一個 Widget 時,有個 exception 被拋出,則其它 9 個還是應該被 destruct,因此要又喚起第二個 Widget 的 destructor,如果又再次拋出 exception,此時,同時有兩個 exception,這對 C++ 而言太多了,在兩個 exception 同時存在的情況下,程式若不是結束執行,就是導致不明確行為。
所以是的,不要在解構式吐出 exception。
但如果在 destructor 中必須執行一個動作,而該動作可能在失敗時拋出 exception,該怎麼辦?
如此,只要呼叫 close 成功,則一切美好,只如何該呼叫導致 exception,也就是允許它離開 destructor,那就會造成問題。
有兩個方式可以避免在 destructor 中拋出 exception:
1、如果 close() 發出 exception 就結束程式:
2、吞下 close() 發出的 exception :
不過這兩種處理方式都無法即時對「導致 close 拋出 exception」的情況作出反應。
一個較佳的策略是重新設計 DBConn 介面,讓使用者有機會對可能出現的問題作出反應,如 DBConn 可以提供一個 close 函式,讓使用者得以處理「因該操作而發生的異常」:
把呼叫 close 的責任交給使用者,並給使用者一個處理 exception 的機會,如果使用者不想用 close 函式,也可以倚賴 DBConn 的 destructor,但如果 close() 發出 exception,則 DBConn 吞下該 exception 或結束程式。
在 destructor 中,絕對不要吐出 exception,如果其呼叫的函式可能拋出 exception,則吞下它們或結束程式。
如果要讓使用者可以在函式執行期間對函式所拋出的 exception 作處理,則 class 應該提供一個普通的函式執行該操作,而並非是在 destructor。
09、Never call virtual functions during constructor or destruction.
在 constructor 及 destructor 中,不應該呼叫 virtual function,因為這樣子的呼叫,並不會帶來預期的結果,而這也是 C++ 不同於 Java 或 C# 的一個地方:
如果此時,以下程式碼被執行,則會發生什麼事?
可以肯定的,BuyTransaction 的 constructor 會被呼叫,但 Transaction 的 constructor 一定會更早被呼叫,而 constructor of Transaction 中的最後一行,呼叫 virtual function logTransaction,此時,被呼叫的版本是 Transaction 內的版本,是的,在 base class 建構期間,virtual function 絕不會下降到 derived class 階層,非正式的說法是:在 base class 建構期間,virtual function 就不是 virtual function。
相同的道理也適用於解構式,一旦 destructor of derived class 開始執行,物件內的 derived class 成員變數便呈現未定義值,所以在進入destructor of base class 後,也就變成一個 base class 的物件。
在上述例子中,這個問題在執行前是顯而易見的,因為 logTransaction 在 Transaction 內是個 pure virtual function,除非它被定義,否則程式將無法順利進行連結,因為連結器找不到必要的 Transaction::logTransaction 實作碼。
但該如何確保每次一有 Transaction 繼承體系上的物件被產生時,就會有適當版本的 logTransaction 被喚起呢?
一種作法是在 class Transaction 內將 logTransaction 改為 non-virtual,然後要求 constructor of derived class 傳遞必要資訊給 constructor of Transaction,之後 constructor of Transaction 便可安全地呼叫 non-virtual logTransaction:
這個方式,換句話說,也就是在建構期間,藉由「令 derived class 將必要的建構資訊向上傳遞至 constructor of base class」來完成。
要注意的是在本例中,BuyTransaction 內的 private function createLogString 是 static,也就不會意外地使用到「初期未成熟之 BuyTransaction 物件內尚未被初始化的成員變數」。
在 constructor 及 destructor 中不要呼叫 virtual function,因為這類的呼叫從不會下降至 derived class。
10、Have assignment operators return a reference to *this.
有關 assignment ,有趣的是可以將它們寫成連鎖形式:
也因為 assignment 採用右結合律,所以上述連鎖式會被解析為:
z 先被 assign 為 15,而更新後整的值再 assign 給 y,而 y 更新後的值再 assign 給 x。
為了實作「連鎖賦值」,assignment operator 必須回傳一個 reference 指向 operator 的左側引數,這也是為 classes 實作 assignment operators 時,該遵守的協定:
然而這個協定不只適用於上述的標準 assignment 形式,也適用於所有 assignment 相關運算:
要注意的是這只是個協定,就算不遵循,程式碼一樣可以通過編譯,但這份協定被所有內建型別及標準程式庫提供的型別或即將提供的型別(見條款 54)所共同遵守。
令 assignment operator 傳回一個 reference to *this。
11、Handle assignment to self in operator=.
「自我賦值」發生在物件被賦值給自己的時候:
賦值動作,並不總是輕易就可辨識出來:
這些不明顯的自我賦值,是「別名」(aliasing) 帶來的結果:
所謂「別名」就是「有一個以上的方式指向某物件」。
如以下程式碼,表面上看起來合理,但自我賦值時並不安全,同時它也不具備異常安全性:
這裡自我賦值的問題是,operator= 函式內的 *this 和 rhs 有可能是同一物件,則 delete pb 時,會發生問題。
要阻止這種錯誤的傳統作法是利用「證同測試」(identity test),來達到「自我賦值」的檢驗目的:
這個作法是行得通的,但只具備「自我賦值安全性」,不具備「exception safety」,如果在 "new Bitmap" 導致 exception 發生,則 Widget 最終會有一個指標指向一塊被刪除的 Bitmap。
而令人高興的,讓 operator= 具備「exception safety」,通常也會自動獲得「自我賦值安全」的回報,所以只需要把焦點放在實作「exception safety」即可,在條款 29 中深度探討了 exception safety,但此時,只要注意「許多時候,一群精心安排的述句,就可以導出 exception safety 及自我賦值安全的程式碼」:
此時,就算 "new Bitmap" 拋出 exception,pb 還是能保持原狀,而即使沒作 identity test,這段程式碼也還是能處理自我賦值。
在 operator= 函式內使用精心排列述句的另一個替代方案是:
使用 copy and swap 技術,這個技術和 exception safety 有密切關係,在條款 29 中詳細說明:
另一個變形的方式是利用:
1、某 class 內的 copy assignment operator 可能被宣告為「以 by value 方式接受引數」。
2、以 by value 方式傳遞物件會造成一份複件。
這個作法為了巧妙的修補,而犧牲了程式的清晰性,然而將「copying 動作」從函式本體移至「函式參數建構階段」卻可以編譯器產生更高效的程式碼。
確保當物件自我賦值時,operator= 會有良好的行為,其中技術包括:identity test、精心排列的述句順序以及 copy and swap。
確定操作一個以上的物件的任何函式,在其中多個物件是同一個物件時,也能正常執行。
12、Copy all parts of an object.
良好的物件導向系統會將物件內部封裝起來,只留兩個函式負責物件拷貝,也就是帶著適切名稱的 copy constructor 及 copy assignment operator,這裡稱它們為 copying function。
條款 5 中提到,編譯會在會必要時,為 classes 產生 copying function,除非 class 已經有宣告 copying function,然而在已經有宣告 copying function 的情況下,就算實作碼幾乎必然出錯的情況,編譯器也不會發出通知。
這個例子中,每樣事情似乎看起來都很好,而實際上也的確很好,直到加入另一個變數:
此時,會造成既有的 copying function 是 partial copy,它們有複製了 name ,但卻忽略了 lastTransaction,而編譯器通常不會對此發出什麼意見,所以很明顯地:
如果為 class 添加一個變數,必須同時修改 copy function,同時也需要修改所有的 constructor,及任何非標準型式的 operator=(條款10有個例子)。
類似的情況,也可能發生在繼承:
PriorityCustomer 的 copying function 只複製了 PriorityCustomer 宣告的成員變數,卻忽略了 PriorityCustomer 所內含的 Customer 的成員變數,在 PriorityCustomer 的 copy constructor 中,PriorityCustomer 的 Customer 成分會被不帶引數的 Customer constructor 初始化。
而 PriorityCustomer 的 copy assignment operator 也是有類似的情況,在 base class 的成員變數將不會有任何變動。
此時正確的作法,應該要讓 derived class 的 copying function 呼叫相應的 base class function:
所以,當在編寫一個 copying function 時,須確保:
1、複製所以 local 成員變數。
2、呼叫所有 base classes 內的適當 copying function
在 copy constructor 及 copy assignment operator 中,往往有近似的實作本體,但從任一個 function 呼叫到另一個 function 都是行不通的:
令 copy assignment operator 呼叫 copy constructor 是不合理的,因為這就像試圖建構一個已經存在的物件。
令 copy constructor 呼叫 copy assignment operator 同樣無意義,因為建構式尚未完成,怎麼呼叫它的 copy assignment operator?!
要消除重複程式碼,合適的作法是,建立一個新的 member function 給這兩個函式呼叫,這樣的函式往往是 private,而且常被命名為 init。
Copying function 應該確保複製「物件內的所有 data member」及「所有 base class 內的 data member」。
不要嘗試以某個 copying function 呼叫另一個 copying function,而是應該就共同機能放進第三個 function 中,並由兩個 copying function 呼叫。
幾乎每個 classes 都會有一個或多個建構式、一個解構式及一個 copy assignment operator,學著控制這些基礎操作,像是產出新物件並確保它被初始化、擺脫舊物件並確保它被適當處理及賦與物件新值,如果這些函式犯錯,會導致深遠且令人不愉快的後果,且將遍及整個 classes,本章的引導,將可把這些函式良好地集結在一起,形成 classes 的脊柱。
05、Know what functions C++ silently writes and calls.
什麼時候 empty class (空類別) 將不再是個 empty class 呢?當 C++ 處理過它之後。
是的,如果 class 本身沒有宣告,編譯器就會為它宣告一個 copy constructor、一個 copy assignment operator 和一個 destructor,如果沒有宣告任何的 constructor,則編譯器也會再宣告一個 default constructor,而所有的這些函式都是 public 且 inline 的,詳見條款 30。
因此,如果寫下:
class Empty { };
效果等同於:
class Empty {
public:
Empty(); // default constructor
Empty(const Empty& rhs) { ... } // copy constructor
~Empty(); // destructor, might be virtual
Empty& operator=(const Empty& rhs) { ... }
// copy assignment operator
};
不過也唯有這些函式被需要時(被呼叫),它們才會被編譯器產生出來。
編譯器為何需要自動產生這些函式?
default constructor 及 destructor 主要是給編譯器一個地方用來放置「藏身幕後」的程式碼,像是喚起 base class 和 non-static 成員變數的 constructor 及 destructor。
如果使用者自行宣告了一個 constructor,則編譯器將不再為它產生 default constructor。
一般來說,編譯器所產生的 destructor 是 non-virtual (見條款7),除非這個 class 的 base class 本身有宣告 virtual destructor,這種情況下,這個函式的虛擬屬性(virtualness) 主要來自 base class。
至於 copy constructor 及 copy assignment operator,編譯器產生的版本只是單純地將來源物件的每個 non-static 成員變數拷貝到目標物件。
template
class NamedObject {
public:
NamedObject(const char* name, const T& value);
NamedObject(const std::string& name, const T& value);
...
private:
std::string nameValue;
T objectValue;
};
NamedObject,由於已經宣告 constructor,所以編譯器將不再為它產生 default constructor。
但也由於沒有宣告 copy constructor 及 copy assignment operator,所以如果這些函數有被呼叫的話,編譯器將會自動為它產生。
NamedObject no1("Smallest Prime Number", 2);
NamedObject no2(no1); // invoke copy constructor
編譯器生成的 copy constructor 必須以 no1.nameValue 及 no1.objectValue 為初值,來設定 no2.nameValue 及 no2.objectValue。
其中 nameValue 的型別是 string,所以 no2.nameValue 初始化的方式是喚起 string 的 copy constructor 並以 no1.nameValue 為引數。
另一個成員 objectValue 的型別是 int,是一個內建型別,所以 no2.objectValue 會以「拷貝 no1.objectValue 內的每個 bits」來完成初始化。
編譯器為 NamedObject
對上述例子,作些修改:
template
class NamedObject {
public:
NamedObject(std::string& name, const T&value);
...
private:
std::string& nameValue; // 如今這是個 reference
const T objectValue; // 如今這是個 const
};
考慮下列操作,將會發生什麼事:
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject p(newDog, 2);
NamedObject s(oldDog, 36);
p = s; // 此時 p 的成員變數該發生什麼事?
由於 C++ 並不允許「讓 reference 改指向不同物件」,也由於修改 p.nameValue 所指的那個 string ,會進而影響「持有 pointers 或 references 指向該 string 」的其它物件,所以 C++ 對於這之類的 copy assignment operator 的回應是拒絕編譯那一行賦值動作,此時,就需要自行定義 copy assignment operator。
而面對 「內含 const 成員」的 classes,編譯器的反應也是相同,因為更改 const 成員是不合法的。
還有一種情況,編譯器也會拒絕為其產生 copy assignment operator:
如果某個 base classes 將 copy assignment operator 宣告為 private,則編譯器將拒絕為其 derived classes 生成 copy assignment operator。
編譯器可以暗自為 class 產生: default constructor、copy constructor、copy assignment operator 及 destructor。
06、Explicitly disallow the use of compiler-generated functions you do not want.
通常如果不希望 class 支援某些特定機能,只要不宣告相對應的函式即可,不過這個策略,對於 copy constructor 及 copy assignment operator 卻不起作用,因為如條款5 所提:
如果沒有宣告它們,而在它們被呼叫時,編譯器將會自動產生這些函式。
而到底該如何作,才能阻止 class 支援 copying 呢?
答案的關鍵是:預防 copy constructor 及 copy assignment operator 被自動產生出來。
作法是:將 copy consctuctor 及 copy assignment operator 宣告為 private。
如此,將阻止了編譯器會暗自產生其專屬版本的函式。
將 copy constructor 及 copy assignment operator 宣告為 private 之後,為了預防 member function 及 friend class 的呼叫,這兩個 function 將不實作出來,如此,如果不慎呼叫了任何一個檔案,會獲得一個連結錯誤(linkage error)。
「將成員函式宣告為 private,而且故意不實作它們」這一技倆是廣被使用的,在標準程式庫實作碼中的 ios_base, basic_ios, entry ,無論是哪一個,其 copy constructor 和 copy assignment operator ,也都被宣告為 private ,而且也都沒定義。
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&); // 只有宣告
HomeForSale& operator=(const HomeForSale&); // 只有宣告
}
有了上述 class 的定義,如果不慎在 member function 或 friend function 企圖拷貝 HomeForSale,則連結器會發出抱怨,如果是其它 function ,則編譯器會進行阻止。
不過有沒有辦法讓錯誤在編譯期時就被發現?
答案是有的,但要依靠一個專門為阻止 copying 動作而設計的 base class:
class Uncopyable {
protected:
Uncopyable(); // 允許 derived 物件的建構和解構
~Uncopyable();
private:
Uncopyable(const Uncopyable&); // 但阻止 copying
Uncopyable& operator=(const Uncopyable&);
};
而為了阻止 HomeForSale 物件被拷貝,唯一需要做的就是繼承 Uncopyable:
class omeForSale : private Uncopyable {
...
};
原因在於,只要有任何嘗試要拷貝 HomeForSame 物件,則編譯器便試著產生一個 copy constructor 和一個 copy assignment operator,但如條款 12 所說,這些「編譯器生成版」函式會嘗試呼叫 base class 的對應兄弟,但那些呼叫會被編譯器拒絕,因為 base class 的拷貝函式是 private。
為駁回編譯器自動提供的機能,可將相應的成員函式宣告為 private 並且不予實作,又或是使用像 Uncopyable 這樣的 base class 也是一種作法。
07、Declare destructors virtual in polymorphic base classes.
有很多種方式可以記錄時間,因此設計一個 TimeKeeper base class 和一些 derived classes 作為不同的計時方式,是相當合理的:
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock : public TimeKeeper() { ... };
class WaterClock : public TimeKeeper() { ... };
class WristClock : public TimeKeeper() { ... };
有時在指標的設計上為了方便,會設計一個 factory function,factory function 會「傳回一個指向新生成之 derived class 物件的 base class 指標」:
TimeKeeper* getTimeKeeper(); // 傳回指向 derived class 的指標
而呼叫的方式為:
TimeKeeper* pkt = getTimeKeeper(); // 取得的態配置物件
...
delete pkt; // 釋放資源
此時,先不管條款 13 說「依賴客戶執行 delete 動作,基本上便帶有某種錯誤傾向」,也不管條款 18 談到 factory function 介面該如何修改以預防常見之客戶錯誤。
上述程式碼有一個更根本的缺點,即使把每樣事情都做對了,仍沒辦法知道程式如何行動。
問題點是 getTimeKeeper() 所回傳的是 derived class 物件,而這個物件被 base class 指標指的,而且被 delete,然而在 base class 卻有一個 non-virtual destructor 。
C++ 明白指出,當 derived class 經由一個 base class 指標被刪除,而該 base class 帶著一個 non-virtual destructor ,這個結果是未有定義的,而實際上的情況是:物件的 derived 成分沒被 destruct,base class 成分通常會被 destruct。
消除這個問題的方法是:給 base class 一個 virtual destructor,此後 delete derived class 物件就會 destruct 整個物件:
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper* pkt = getTimeKeeper();
...
delete pkt;
像 TimeKeeper 這類的 base class,除了 destructor 之外,通常還有其它 virtual function,因為 virtual function 的目的是允許 derived classes 的實作可以客製作,詳見條款 34。
而任何 class 只要帶有 virutal function,也幾乎確定應該也要有一個 virtual destructor。
但如果無端將 destructor 宣告為 virtual ,往往是個餿主意,因為要實作出 virtual function,物件必攜帶某些資訊,主要用來決定在執行期哪一個 virtual funcion 該被喚起,這份資料由一個 vptr 的指標指出,vptr 指向一個由函式指標構成的陣列,稱為 vtbl,每個帶有 virtual function 的 class 都有一個相應的 vtable,這將增加物件的體積,也會導致無法將物件傳遞至其它語言所寫的函式。。
即使 class 完全不帶 virtual function,也是會有用法錯誤的情況:
class SpecialString : public std::string {
// bad idea, std::string 有個 non-virtual destructor
...
};
SpecialString* pss = new SpecialString("Impending Doom");
std::string* ps;
...
ps = pss;
...
delete(ps);// 未定義,現實情況會有 memory leak
相同的分析適用於所有不帶 virtual destructor 的 class,包括所有 STL 容器,如:vector, list, set, tr1::unordered_map,所以,如果想繼承一個標準容器或其它「帶有 non-virtual destructor」的 class,拒絕誘惑吧!
有時候,會希望某個 class 是 abstract class ,但卻沒有任何的 pure virtual function,此時,可以為這個 class 宣告一個 pure virtual destructor:
class AWOV { // Abstract Without Virtuals public: virtual ~AWOV() = 0; // 宣告 pure virtual destructor }; //
這個 class 有個 pure virtual 函式,所以是一個 abstract class,同時它有個 virtual destructor ,所以不必擔心 destructor 的問題。
然而,卻必須為 pure virtual destructor 提供一份定義:
AWOV::~AWOF() { }
destructor 的運作方式是: the most derived class 會最先被解構,然後是每個 base class 的 destructor 被呼叫,而編譯器在 AWOV 的 derived class 的 destructor 中,會產生一個對 ~AWOV() 的呼叫,所以必需為這個函式提供定義,否則連結器會發出抱怨。
「給 base classes 一個 virtual destructor」,這個規則只適用於 polymorphic base classes 身上,其設計目的是為了用來「透過 base class 介面處理 derived class 物件」。
polymorphic base classes 應該宣告一個 virtual destructor,如果 class 帶有任何 virtual function,也應該擁有一個 virtual destructor。
Classes 的設計目的如果不是為了作為 base class,或不是具備 polymorphically,就不應該宣告 virtual destructor。
A class that declares or inherits a virtual function is called a polymorphic class.
08、Prevent exceptions from leaving destructors.
C++ 雖然不禁止 destructor 吐出 exception,但這樣子的作法並不被鼓勵,考慮以下的程式碼:
class Widget {
public:
...
~Widget() { ... } // 假設這裡可能吐出 exception
};
void doSomething() {
std::vector v;
... // v 在這裡被 destruct
}
當 vector v 被 destruct 時,它有責任將其內含的所有 Widgets 全部 destruct,假設含有 10 個 widgets,如果在 destruct 第一個 Widget 時,有個 exception 被拋出,則其它 9 個還是應該被 destruct,因此要又喚起第二個 Widget 的 destructor,如果又再次拋出 exception,此時,同時有兩個 exception,這對 C++ 而言太多了,在兩個 exception 同時存在的情況下,程式若不是結束執行,就是導致不明確行為。
所以是的,不要在解構式吐出 exception。
但如果在 destructor 中必須執行一個動作,而該動作可能在失敗時拋出 exception,該怎麼辦?
class DBConnection {
public:
...
static DBConnection create(); // 傳回 DBConnection 物件
void close(); // 關閉連線,失敗則拋出 exception
};
class DBConn { // 用來管理 DBConnection 物件
public:
...
~DBCon() { // 確保資料庫連線總是會被關閉
db.close();
}
private:
DBConnection db;
};
如此,只要呼叫 close 成功,則一切美好,只如何該呼叫導致 exception,也就是允許它離開 destructor,那就會造成問題。
有兩個方式可以避免在 destructor 中拋出 exception:
1、如果 close() 發出 exception 就結束程式:
DBConn::~DBConn() {
try { db.close(); }
catch (...) {
... // 記下對 close() 的呼叫失敗
std::abort(); // 結束程式
}
};
2、吞下 close() 發出的 exception :
DBConn::~DBConn() {
try { db.close(); }
catch (...) {
... // 記下對 close() 的呼叫失敗
}
};
不過這兩種處理方式都無法即時對「導致 close 拋出 exception」的情況作出反應。
一個較佳的策略是重新設計 DBConn 介面,讓使用者有機會對可能出現的問題作出反應,如 DBConn 可以提供一個 close 函式,讓使用者得以處理「因該操作而發生的異常」:
class DBConn { // 用來管理 DBConnection 物件
public:
...
void close() { 供使用者使用的新函式
db.close();
closed = true;
}
~DBCon() { // 確保資料庫連線總是會被關閉
if (!closed) {
try () {
db.close();
} catch (...) {
... // 記下對 close() 的呼叫失敗
}
}
}
private:
DBConnection db;
closed;
};
把呼叫 close 的責任交給使用者,並給使用者一個處理 exception 的機會,如果使用者不想用 close 函式,也可以倚賴 DBConn 的 destructor,但如果 close() 發出 exception,則 DBConn 吞下該 exception 或結束程式。
在 destructor 中,絕對不要吐出 exception,如果其呼叫的函式可能拋出 exception,則吞下它們或結束程式。
如果要讓使用者可以在函式執行期間對函式所拋出的 exception 作處理,則 class 應該提供一個普通的函式執行該操作,而並非是在 destructor。
09、Never call virtual functions during constructor or destruction.
在 constructor 及 destructor 中,不應該呼叫 virtual function,因為這樣子的呼叫,並不會帶來預期的結果,而這也是 C++ 不同於 Java 或 C# 的一個地方:
class Transaction { // 所有交易的 base class public: Transaction(); virtual void logTransaction() const = 0; // pure virtual function for writing log ... }; Transaction::Transaction() { // constructor of base class ... logTransaction(); // writing the log } class BuyTransaction : public Transaction { // derived class public: virtual void logTransaction() const; // log this transaction ... }; class SellTransaction : public Transaction { // derived class public: virtual void logTransaction() const; // log this transaction ... }; //
如果此時,以下程式碼被執行,則會發生什麼事?
BuyTransaction b;
可以肯定的,BuyTransaction 的 constructor 會被呼叫,但 Transaction 的 constructor 一定會更早被呼叫,而 constructor of Transaction 中的最後一行,呼叫 virtual function logTransaction,此時,被呼叫的版本是 Transaction 內的版本,是的,在 base class 建構期間,virtual function 絕不會下降到 derived class 階層,非正式的說法是:在 base class 建構期間,virtual function 就不是 virtual function。
相同的道理也適用於解構式,一旦 destructor of derived class 開始執行,物件內的 derived class 成員變數便呈現未定義值,所以在進入destructor of base class 後,也就變成一個 base class 的物件。
在上述例子中,這個問題在執行前是顯而易見的,因為 logTransaction 在 Transaction 內是個 pure virtual function,除非它被定義,否則程式將無法順利進行連結,因為連結器找不到必要的 Transaction::logTransaction 實作碼。
但該如何確保每次一有 Transaction 繼承體系上的物件被產生時,就會有適當版本的 logTransaction 被喚起呢?
一種作法是在 class Transaction 內將 logTransaction 改為 non-virtual,然後要求 constructor of derived class 傳遞必要資訊給 constructor of Transaction,之後 constructor of Transaction 便可安全地呼叫 non-virtual logTransaction:
class Transaction { // 所有交易的 base class
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo);
// non-virtual function for writing log
...
};
Transaction::Transaction(const std::string& logInfo) { // constructor of base class
...
logTransaction(); // writing the log
}
class BuyTransaction : public Transaction { // derived class
public:
BuyTransaction( parameters )
: Transaction(createLogString( parameters )) {
// 將 log 資訊傳給 constructor of base class
...
}
private:
static std::string createLogString( parameters );
};
這個方式,換句話說,也就是在建構期間,藉由「令 derived class 將必要的建構資訊向上傳遞至 constructor of base class」來完成。
要注意的是在本例中,BuyTransaction 內的 private function createLogString 是 static,也就不會意外地使用到「初期未成熟之 BuyTransaction 物件內尚未被初始化的成員變數」。
在 constructor 及 destructor 中不要呼叫 virtual function,因為這類的呼叫從不會下降至 derived class。
10、Have assignment operators return a reference to *this.
有關 assignment ,有趣的是可以將它們寫成連鎖形式:
int x, y, z;
x = y = z = 13; // 連銷賦值
也因為 assignment 採用右結合律,所以上述連鎖式會被解析為:
x = (y = (z = 13));
z 先被 assign 為 15,而更新後整的值再 assign 給 y,而 y 更新後的值再 assign 給 x。
為了實作「連鎖賦值」,assignment operator 必須回傳一個 reference 指向 operator 的左側引數,這也是為 classes 實作 assignment operators 時,該遵守的協定:
class Widget {
public:
...
Widget& operator=(const Widget& rhs) {
...
return *this;
}
};
然而這個協定不只適用於上述的標準 assignment 形式,也適用於所有 assignment 相關運算:
class Widget {
public:
...
Widget& operator+=(const Widget& rhs) {
// 這個協定適用於 +=, -=, *= 等等
...
return *this;
}
Widget& operator=(int rhs) {
// 即使參數形別不符合協定,也可以適用
...
return *this;
}
};
要注意的是這只是個協定,就算不遵循,程式碼一樣可以通過編譯,但這份協定被所有內建型別及標準程式庫提供的型別或即將提供的型別(見條款 54)所共同遵守。
令 assignment operator 傳回一個 reference to *this。
11、Handle assignment to self in operator=.
「自我賦值」發生在物件被賦值給自己的時候:
class Widget { ... };
Widget w;
...
w = w; // 賦值給自己
賦值動作,並不總是輕易就可辨識出來:
a[i] = a[j]; // 若 i == j,則賦值給自己
*px = *py; // 若 px == py,則賦值給自己
這些不明顯的自我賦值,是「別名」(aliasing) 帶來的結果:
所謂「別名」就是「有一個以上的方式指向某物件」。
如以下程式碼,表面上看起來合理,但自我賦值時並不安全,同時它也不具備異常安全性:
class Bitmap { ... };
class Widget {
...
private:
Bitmap* pb; // 指向一個從 heap 配置而得的物件
};
Widget& Widget::operator=(const Widget& rhs) {
// 不安全的 operator= 實作版本
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
這裡自我賦值的問題是,operator= 函式內的 *this 和 rhs 有可能是同一物件,則 delete pb 時,會發生問題。
要阻止這種錯誤的傳統作法是利用「證同測試」(identity test),來達到「自我賦值」的檢驗目的:
Widget& Widget::operator=(const Widget& rhs) {
if (this == rhs) return *this; // identity test
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
這個作法是行得通的,但只具備「自我賦值安全性」,不具備「exception safety」,如果在 "new Bitmap" 導致 exception 發生,則 Widget 最終會有一個指標指向一塊被刪除的 Bitmap。
而令人高興的,讓 operator= 具備「exception safety」,通常也會自動獲得「自我賦值安全」的回報,所以只需要把焦點放在實作「exception safety」即可,在條款 29 中深度探討了 exception safety,但此時,只要注意「許多時候,一群精心安排的述句,就可以導出 exception safety 及自我賦值安全的程式碼」:
Widget& Widget::operator=(const Widget& rhs) {
Bitmap* pOrig = pb; // 記住原先的 pb
pb = new Bitmap(*rhs.pb); // 令 pb 指向另一個複件
delete pOrig; // 刪除原先的 pb
return *this;
}
此時,就算 "new Bitmap" 拋出 exception,pb 還是能保持原狀,而即使沒作 identity test,這段程式碼也還是能處理自我賦值。
在 operator= 函式內使用精心排列述句的另一個替代方案是:
使用 copy and swap 技術,這個技術和 exception safety 有密切關係,在條款 29 中詳細說明:
class Widget {
...
void swap(Widget& rhs); // 交換 *this 和 rhs 的資料
};
Widget& Widget::operator=(const Widget& rhs) {
Widget temp(rhs); // 為 rhs 資料製作一份複件
swap(temp); // 將 *this 資料和上述複件的資料交換
return *this;
}
另一個變形的方式是利用:
1、某 class 內的 copy assignment operator 可能被宣告為「以 by value 方式接受引數」。
2、以 by value 方式傳遞物件會造成一份複件。
Widget& Widget::operator=(Widget rhs) { // pass by value
swap(rhs);
return *this;
}
這個作法為了巧妙的修補,而犧牲了程式的清晰性,然而將「copying 動作」從函式本體移至「函式參數建構階段」卻可以編譯器產生更高效的程式碼。
確保當物件自我賦值時,operator= 會有良好的行為,其中技術包括:identity test、精心排列的述句順序以及 copy and swap。
確定操作一個以上的物件的任何函式,在其中多個物件是同一個物件時,也能正常執行。
12、Copy all parts of an object.
良好的物件導向系統會將物件內部封裝起來,只留兩個函式負責物件拷貝,也就是帶著適切名稱的 copy constructor 及 copy assignment operator,這裡稱它們為 copying function。
條款 5 中提到,編譯會在會必要時,為 classes 產生 copying function,除非 class 已經有宣告 copying function,然而在已經有宣告 copying function 的情況下,就算實作碼幾乎必然出錯的情況,編譯器也不會發出通知。
void logCall(const std::string& funcName); // 製造一個 log entry
class Customer {
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
Customer::Customer(const Customer& rhs)
: name(rhs.name) {
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs) {
logCall("Customer copy assignment operator");
name = rhs.name;
return *this;
}
這個例子中,每樣事情似乎看起來都很好,而實際上也的確很好,直到加入另一個變數:
class Customer {
public:
...
private:
std::string name;
Date lastTransaction;
};
此時,會造成既有的 copying function 是 partial copy,它們有複製了 name ,但卻忽略了 lastTransaction,而編譯器通常不會對此發出什麼意見,所以很明顯地:
如果為 class 添加一個變數,必須同時修改 copy function,同時也需要修改所有的 constructor,及任何非標準型式的 operator=(條款10有個例子)。
類似的情況,也可能發生在繼承:
class PriorityCustomer : public customer {
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority) {
logCall("PriorityCustomercopy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
logCall("PriorityCustomer copy assignment operator");
priority= rhs.priority;
return *this;
}
PriorityCustomer 的 copying function 只複製了 PriorityCustomer 宣告的成員變數,卻忽略了 PriorityCustomer 所內含的 Customer 的成員變數,在 PriorityCustomer 的 copy constructor 中,PriorityCustomer 的 Customer 成分會被不帶引數的 Customer constructor 初始化。
而 PriorityCustomer 的 copy assignment operator 也是有類似的情況,在 base class 的成員變數將不會有任何變動。
此時正確的作法,應該要讓 derived class 的 copying function 呼叫相應的 base class function:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs) // 呼叫 base class 的 copy constructor
, priority(rhs.priority) {
logCall("PriorityCustomercopy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs);
// 呼叫 base class 的 assignment operator
priority= rhs.priority;
return *this;
}
所以,當在編寫一個 copying function 時,須確保:
1、複製所以 local 成員變數。
2、呼叫所有 base classes 內的適當 copying function
在 copy constructor 及 copy assignment operator 中,往往有近似的實作本體,但從任一個 function 呼叫到另一個 function 都是行不通的:
令 copy assignment operator 呼叫 copy constructor 是不合理的,因為這就像試圖建構一個已經存在的物件。
令 copy constructor 呼叫 copy assignment operator 同樣無意義,因為建構式尚未完成,怎麼呼叫它的 copy assignment operator?!
要消除重複程式碼,合適的作法是,建立一個新的 member function 給這兩個函式呼叫,這樣的函式往往是 private,而且常被命名為 init。
Copying function 應該確保複製「物件內的所有 data member」及「所有 base class 內的 data member」。
不要嘗試以某個 copying function 呼叫另一個 copying function,而是應該就共同機能放進第三個 function 中,並由兩個 copying function 呼叫。
全站熱搜