天天看點

讀書筆記 effective c++ Item 8 不要讓異常(exceptions)離開析構函數

1.為什麼c++不喜歡析構函數抛出異常

C++并沒有禁止析構函數出現異常,但是它肯定不鼓勵這麼做。這是有原因的,考慮下面的代碼:

1 class Widget {
 2 
 3 public:
 4 
 5 ...
 6 
 7 ~Widget() { ... } // assume this might emit an exception
 8 
 9 };
10 
11 void doSomething()
12 
13 {
14 
15 std::vector<Widget> v;
16 
17 ...
18 
19 } // v is automatically destroyed here      

當vector V被銷毀,V有責任将它包含的所有Widgets都銷毀。假設v含有有10個Widgets對象,當銷毀第一個Widgets對象時,抛出了一個異常。其餘的9個仍然需要被釋放掉(否則它們擁有的資源會被洩露),是以V應該觸發其餘9個對象所有的析構函數。但是假設在這9個析構函數調用過程中,第二個Widget的析構函數抛出了一個異常。現在有兩個主動抛出的異常了,這對c++來說太多了。在兩個異常同時出現的情況下,程式的執行要麼終止要麼産生未定義行為。在這個例子中,它會産生未定義行為。使用任何其他标準庫容器(如list或set)或者TR1中的容器,甚至一個數組也将會産生同樣的未定義行為。出現這種麻煩并不隻是在容器或者數組中出現。在不使用容器或者數組的情況下,析構函數抛出的異常也可以使程式過早終止或者出現未定義行為。C++不喜歡析構函數發出異常!

2.一個例子-DB資源管理類

這很容易了解,但是析構函數需要執行的操作有可能由于異常被抛出而導緻失敗,這時候我們應該怎麼做?舉個例子,假設你在實作一個關于資料庫連接配接的類:

1 class DBConnection {
 2 
 3 public:
 4 
 5 ...
 6 
 7 static DBConnection create(); // function to return
 8 
 9 // DBConnection objects; params
10 
11 // omitted for simplicity
12 
13 void close(); // close connection; throw an
14 
15 }; // exception if closing fails      

為了確定用戶端不會忘記調用DBConnection對象的close函數,為DBConnestion建立一個資源管理類是一個理想的方法,close函數會在資源管理類的析構函數中被調用。這樣的資源管理類将在第三章有詳細的講述,在這裡,考慮這樣一個類的析構函數會長成什麼樣子就足夠了:

1 class DBConn { // class to manage DBConnection
 2 
 3 public: // objects
 4 
 5 ...
 6 
 7 ~DBConn() // make sure database connections
 8 
 9 { // are always closed
10 
11 db.close();
12 
13 }
14 
15 private:
16 
17 DBConnection db;
18 
19 };      

于是用戶端代碼可以寫成這樣:

1 { // open a block
 2 
 3 DBConn dbc(DBConnection::create()); // create DBConnection object
 4 
 5 // and turn it over to a DBConn
 6 
 7 // object to manage
 8 
 9 ... // use the DBConnection object
10 
11 // via the DBConn interface
12 
13 } // at end of block, the DBConn
14 
15 // object is destroyed, thus
16 
17 // automatically calling close on
18 
19 // the DBConnection object      

隻要close函數的調用成功了這個實作就是很好的,但是如果調用産生一個異常,DBConn的析構函數會傳播這個異常,也就是允許異常離開析構函數。這是一個問題,因為在析構函數中發生throw就意味這麻煩。

3.如何阻止析構函數中的異常被傳播出去

有兩種方法來避免這個麻煩。DBConn的析構函數可以這麼做:

3.1用abort函數使程式終止

如果close函數抛出異常就将程式終止,可以調用abort函數:

1 DBConn::~DBConn()
 2 
 3 {
 4 
 5 try { db.close(); }
 6 
 7 catch (...) {
 8 
 9 make log entry that the call to close failed;
10 
11 std::abort();
12 
13 }
14 
15 }      

如果在執行析構函數的時候遇到一個錯誤程式就不能繼續運作了,上面的做法會是一個合理的選擇。它的優點是能夠阻止異常從析構函數傳播出去,傳播異常會導緻未定義行為。是以,對于未定義行為,調用abort能夠先發制人。

3.2 将異常吞掉

将調用close時抛出的異常吞掉

1 DBConn::~DBConn()
 2 
 3 {
 4 
 5 try { db.close(); }
 6 
 7 catch (...) {
 8 
 9 make log entry that the call to close failed;
10 
11 }
12 
13 }      

在一般情況下,将異常吞掉是一個壞的方法,因為它會抑制重要錯誤資訊-有一些失敗的事情-的出現!但是有時候,比起程式過早終止或者未定義行為,将異常吞掉會是更好的方法。這是一個可行的選擇,程式必須能夠可靠的繼續執行下去甚至在碰到錯誤出現然後将其忽略的情況。

這兩種方法都不是特别吸引人。這兩種的方法的問題是,程式沒有辦法在第一時間對導緻close抛出異常的條件做出反應。

4.一個更好的方法-使類能夠對異常做出反應

一個更好的方法是對DBConn的接口進行設計,于是用戶端有機會對可能出現的問題做出反應。舉個例子,DBConn類自己可以提供一個close函數,這就可以給用戶端一個處理從close抛出異常的機會,同時也能夠追蹤DBConnection是否已經被關掉了,如果在close中沒有被關掉就在析構函數中再次執行。這就阻止了連接配接無法被正确釋放。如果在DBConn的析構函數中對close的調用将會失敗,我們還得使用終止程式或者吞掉異常的方法:

1 class DBConn {
 2 
 3 public:
 4 
 5 ...
 6 
 7 void close() // new function for
 8 
 9 { // client use
10 
11 db.close();
12 
13 closed = true;
14 
15 }
16 
17 ~DBConn()
18 
19 {
20 
21 if (!closed) {
22 
23 try { // close the connection
24 
25 db.close(); // if the client didn’t
26 
27 }
28 
29 catch (...) { // if closing fails,
30 
31 make log entry that call to close failed; // note that and
32 
33 ... // terminate or swallow
34 
35 }
36 
37 }
38 
39 }
40 
41 private:
42 
43 DBConnection db;
44 
45 bool closed;
46 
47 };      

将調用close的責任從DBConn的析構函數轉移到DBConn的用戶端(因為DBConn的析構函數有一個“備份”調用)可能會給你肆無忌憚轉移負擔的印象。你可能甚至将這種做法當成Item18給出意見的反例(使接口容易被正确使用)。事實上,這兩種想法都是錯的。如果一個操作有可能因為抛出異常而導緻失敗,而我們有可能需要去處理這個異常,這個異常必須來自非析構函數才可以。因為析構函數抛出異常是很危險的,常常會導緻程式過早終止或者未定義行為。在這個例子中,告訴用戶端自己調用close函數并沒有給它們增加負擔;這反而給了它們一個處理錯誤的機會,否則就沒有機會對錯誤做出反應了。如果他們發現這個機會沒有什麼用(可能因為他們相信沒有錯誤會發生),他們可以忽略它,僅依靠DBConn的析構函數在調用close。如果這時出現了錯誤-close确實抛出了異常-他們沒有資格抱怨DBConn吞掉了異常或者終止了程式。畢竟,他們原來有機會處理這個問題,但是他們沒有這麼做。 

5.總結

  • 析構函數不能夠發出任何異常。如果在析構函數中調用某個函數可能會發生throw,析構函數應該catch所有異常然後吞掉他們或者終止程式。
  • 如果類的用戶端需要對一個操作的異常throw做出反應,這個類應該提供一個普通函數來執行這個操作。

作者:

HarlanC

部落格位址:

http://www.cnblogs.com/harlanc/

個人部落格:

http://www.harlancn.me/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出,

原文連結

如果覺的部落客寫的可以,收到您的贊會是很大的動力,如果您覺的不好,您可以投反對票,但麻煩您留言寫下問題在哪裡,這樣才能共同進步。謝謝!

繼續閱讀