C與C++中的異常處理14 (轉)

gugu99發表於2008-08-06
C與C++中的異常處理14 (轉)[@more@]

:namespace prefix = o ns = "urn:schemas--com::office" />

  上次,我開始討論異常安全。這次,我將探究模板安全。

  模板根據引數的型別進行例項化。因為通常事先不知道其具體型別,所以也無法確切知道將在哪兒產生異常。你大概最期望的就是去發現可能在哪兒拋異常。這樣的行為很具挑戰性。

  看一下這個簡單的模板類:

template

class wrapper

  {

public:

  wrapper()

  {

  }

  T get()

  {

  return value_;

  }

  void set(T const &value)

  {

  value_ = value;

  }

private:

  T value_;

  wrapper(wrapper const &);

  wrapper &operator=(wrapper const &);

  };

 

  如名所示,wrapper包容了一個T型別的。方法get()和set()得到和改變私有的包容物件value_。兩個常用方法--複製構造和賦值運算子沒有使用,所以沒有定義,而第三個--解構函式由隱含定義。

  例項化的過程很簡單,例如:

wrapper i;

包容了一個int。i的定義過程導致編譯器從模板例項化了一個定義為wrapper的類:

template <>

class wrapper

  {

public:

  wrapper()

  {

  }

  int get()

  {

  return value_;

  }

  void set(int const &value)

  {

  value_ = value;

  }

private:

  int value_;

  wrapper(wrapper const &);

  wrapper &operator=(wrapper const &);

  };

 

  因為wrapper只接受int或其引用(一個內嵌型別或內嵌型別的引用),所以不會觸及異常。wrapper不拋異常,也沒有直接或間接任何可能拋異常的函式。我不進行正規的分析了,但相信我:wrapper是異常安全的。

 

1.1  class型別的引數

  現在看:

wrapper x;

這裡X是一個類。在這個定義裡,編譯器例項化了類wrapper

template <>

class wrapper

  {

public:

  wrapper()

   {

  }

  X get()

  {

  return value_;

  }

  void set(X const &value)

  {

  value_ = value;

  }

private:

  X value_;

  wrapper(wrapper const &);

  wrapper &operator=(wrapper const &);

  };

 

  粗一看,這個定義沒什麼問題,沒有觸及異常。但思考一下:

l  wrapper包容了一個X的子物件。這個子物件需要構造,意味著呼叫了X的預設建構函式。這個建構函式可能拋異常。

l  wrapper::get()產生並返回了一個X的臨時物件。為了構造這個臨時物件,get()呼叫了X的複製建構函式。這個建構函式可能拋異常。

l  wrapper::set()了value_ = value,它實際上呼叫了X的賦值運算。這個運算可能拋異常。

  在wrapper中針對不拋異常的內嵌型別的操作現在在wrapper中變成呼叫可能拋異常的函式了,同樣的模板,同樣的語句,但極其不同的含義。

  由於這樣的不確定性,我們需要採用保守的策略:假設wrapper會根據類來例項化,而這些類在其成員上沒有異常規格申明,它們可能拋異常。

 

1.2  使得包容安全

  再假設wrapper的異常規格申明承諾其成員不產生異常。至少,我們必須在其成員上加上異常規格申明throw()。我們需要修補掉這些可能導致異常的地方:

l  在wrapper::wrapper()中構造value_的過程。

l  在wrapper::get()中返回value_的過程。

l  在wrapper::set()中對value_賦值的過程。

  另外,在違背throw()的異常規格申明時,我們還要處理std::unexpected。

1.3  Leak #1:預設建構函式

  對wrapper的預設建構函式,解決方法看起來是採用function try塊:

wrapper() throw()

  try : T()

  {

  }

  catch (...)

  {

  }

  雖然很吸引人,但它不能工作。根據C++標準(paragraph 15.3/16,“Handling an exception”):

  對構造或解構函式上的function-try-block,當控制權到達了異常處理函式的結束點時,被捕獲的異常被再次丟擲。對於一般的函式,此時是函式返回,等同於沒有返回值的return語句,對於定義了返回型別的函式此時的行為為未定義。

  換句話說,上面的相當於是:

X::X() throw()

  try : T()

  {

  }

  catch (...)

  {

  throw;

  }

  這不是我們想要的。

  我想過這樣做:

X::X() throw()

  try

  {

  }

  catch (...)

  {

  return;

  }

但它違背了標準的paragraph 15:

  如果在建構函式上的function-try-block的異常處理函式體中出現了return語句,程式是病態的。

  我被標準卡死了,在用支援function try塊的編譯器試驗後,我沒有找到讓它們以我所期望的方式執行的方法。不管我怎麼嘗試,所有被捕獲的異常都仍然被再次丟擲,違背了throw()的異常規格申明,並打敗了我實現介面安全的目標。

 

原則:無法用function try塊來實現建構函式的介面安全。

引申原則1:儘可能使用建構函式不拋異常的基類或成員子物件。

引申原則2:為了幫助別人實現引申原則1,不要從你的建構函式中丟擲任何異常。(這和我在Part13中所提的看法是矛盾的。)

我發現C++標準的規則非常奇怪,因為它們減弱了function try的實際價值:在進入包容物件的建構函式(wrapper::wrapper())前捕獲從子物件(T::T())建構函式中丟擲的異常。實際上,function try塊是你捕獲這樣的異常的唯一方法;但是你只能捕獲它們卻不能處理掉它們!

 

WQ注:下面的文字原載於Part15上,我把提前了。

上次我討論了function try塊的侷限性,並承諾要探究其原因的。我所聯絡的業內專家沒人知道確切答案。現在唯一的共識是:

如我所猜測,標準委員會將function try塊設計為過濾而不是捕獲子物件建構函式中發生的異常的。

可能的動機是:確保沒人誤用沒有構造成功的包容物件。

  我寫信給了Herb Sutter,《teh Exceptional C++》的作者。他從沒碰過這個問題,但很感興趣,以至於將其寫入“Guru of the Week”專欄。如果你想加入這個討論,到新聞組comp.lang.c++.moderated上去看“Guru of the Week #66: Constructor Failures”。

 

  注意function try可以對映或轉換異常:

X::X()

  try

  {

  throw 1;

  }

  catch (int)

  {

   throw 1L; // map int exception to long exception

  }

 

  這樣看,它們非常象unexpected異常的處理函式。事實上,我現在懷疑這才是它們的設計目的(至少是對建構函式而言):更象是個異常過濾器而不是異常處理函式。我將繼續研究下去,以發現這些規則後面的原理。

  現在,至少,我們被迫使用一個不怎麼直接的解決方法:

template

class wrapper

  {

public:

  wrapper() throw()

  : value_(NULL)

  {

  try

  {

  value_ = new T;

  }

  catch (...)

  {

  }

  }

  // ...

private:

  T *value_;

  // ...

  };

 

  被包容的物件,原來是在wrapper::wrapper()進入前構造的,現在是在其函式體內構造的了。這個變化可以讓我們使用普通的方法來捕獲異常而不用function try塊了。

  因為value_現在是個T *而不是T物件了,get()和set()必須使用指標的語法了:

T get()

  {

  return *value_;

  }

 

void set(T const &value)

  {

  *value_ = value;

  }

 

1.4  Leak #1A:operator new

  在建構函式內的try塊中,語句

value_ = new T;

隱含地呼叫了operator new來分配*value_的。而這個operator new函式可能拋異常。

  幸好,我們的wrapper::wrapper()能同時捕獲T的建構函式和operator new函式丟擲的異常,因此維持了介面安全。但,記住這個關鍵性的差異:

l  如果T的建構函式拋了異常,operator delete被隱含呼叫了來釋放分配的記憶體。(對於placement new,這取決於是否存在匹配的operator delete,我在part 8和9說過了的。)

l  如果operator new拋了異常,operator delete不會被隱含呼叫。

  第二點本不該有什麼問題:如果operator new拋了異常,通常是因為記憶體分配失敗,operator delete沒什麼需要它去釋放的。但,如果operator new成功分配了記憶體但因為其它原因而仍然拋了異常,它必須負責釋放記憶體。換句話說,operator new自己必須是行為安全的。

  (同樣的問題也發生在透過operator nwe[]建立陣列時。)

1.5  Leak #1B:Destructor

  想要wrapper行為安全,我們需要它的解構函式釋放new出來的記憶體:

~wrapper() throw()

  {

  delete value_;

  }

  這看起來很簡單,但請等一下說大話!delete value_呼叫*value_的解構函式,而這個解構函式可能拋異常。要實現~wrapper()的介面異常,我們必須加上try塊:

~wrapper() throw()

  {

  try

  {

  delete value_;

  }

  catch (...)

  {

  }

  }

 

  但這還不夠。如果*value_的解構函式拋了異常,operator delete不會被呼叫了來釋放*value_的記憶體。我們需要加上行為安全:

~wrapper() throw()

  {

  try

  {

  delete value_;

  }

  catch (...)

  {

  operator delete(value_);

  }

  }

 

  仍然沒結束。C++標準執行庫申明的operator delete為

void operator delete(void *) throw();

它是不拋異常了,但自定義的operator delete可沒說不拋。要想超級安全,我們應該寫:

~wrapper() throw()

  {

  try

  {

  delete value_;

  }

  catch (...)

  {

  try

  {

  operator delete(value_);

  }

  catch (...)

   {

  }

  }

  }

 

  但這還存在危險。語句

delete value_;

隱含呼叫了operator delete。如果它拋了異常,我們將進入catch塊,一步步執行下去並再次呼叫同樣的operator delete!我們將程式連續暴露在同樣的異常下。這不會是個好程式的。

  最後,記住:operator delete在被new出物件的建構函式拋異常時被隱含呼叫。如果這個被隱含呼叫的operator delete也拋了異常,程式將處於兩次異常狀態並呼叫tenate()。

原則:不要在一個可能在異常正被處理過程被呼叫的函式中拋異常。尤其是,不要從下列情況下拋異常:

l  destructors

l  operator delete

l  operator delete[]

  幾個小習題:用auto_ptr代替value_,然後重寫wrapper的建構函式,並決定其虛構函式的角色(如果需要的話),條件是必須保持異常安全。

1.6  題外話

  我本準備一次維持異常安全的。但現在是第二部分,並仍然有足夠的素材寫成第三部分(我發誓那是最後的部分)。下次,我將討論get()和set()上的異常安全問題,和今天的內容同樣精彩。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-1008586/,如需轉載,請註明出處,否則將追究法律責任。

相關文章