C與C++中的異常處理14 (轉)
: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
包容了一個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
1.1 class型別的引數
現在看:
wrapper
這裡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
l wrapper
l 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塊的侷限性,並承諾要探究其原因的。我所聯絡的業內專家沒人知道確切答案。現在唯一的共識是:
l 如我所猜測,標準委員會將function try塊設計為過濾而不是捕獲子物件建構函式中發生的異常的。
l 可能的動機是:確保沒人誤用沒有構造成功的包容物件。
我寫信給了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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 【C++】 C++異常捕捉和處理C++
- C++異常處理機制C++
- Kotlin DSL C++專案引入OpenCV異常處理(轉)KotlinC++OpenCV
- C++錯誤和異常處理C++
- C++整理19_異常處理C++
- 14. 異常處理
- 【C++】 63_C語言異常處理C++C語言
- C++ 異常處理機制詳解:輕鬆掌握異常處理技巧C++
- (十五)C++學習 | 強制型別轉換 異常處理C++型別
- C#中的異常處理機制C#
- 關於C++ 的異常處理,解答在這來看看吧~C++
- Java 異常表與異常處理原理Java
- 異常處理機制(二)之異常處理與捕獲
- SpringBoot中異常處理Spring Boot
- C#自定義異常 統一異常處理C#
- 29.Spring Boot中異常處理與REST格式處理Spring BootREST
- C++異常C++
- 詳解C#異常處理C#
- 瞭解下C# 異常處理C#
- Ruby中的TypeError異常處理Error
- C介面與實現—C裡面的異常處理機制
- C#基礎之前處理器,異常處理C#
- 異常的處理
- 《C++ Primer》學習筆記(五):迴圈、分支、跳轉和異常處理語句C++筆記
- 異常-throws的方式處理異常
- 異常篇——異常處理
- [轉載] Java異常處理習題Java
- spring中的統一異常處理Spring
- springboot專案中的異常處理Spring Boot
- SpringBoot中的全域性異常處理Spring Boot
- python異常處理中finally的作用Python
- gRPC 中的異常該如何處理?RPC
- Java 中的異常處理機制Java
- 異常處理
- 異常處理與推導式
- C++語言程式設計筆記 - 第12章 - 異常處理C++程式設計筆記
- 在 C++ 中捕獲 Python 異常C++Python
- JSP 異常處理如何處理?JS
- Java中的異常處理最佳實踐Java