更多 C++ 慣用法/多型異常
- 以多型的方式建立異常物件
- 將模組與它可能丟擲的異常的具體細節解耦
依賴倒置原則 (DIP),一個流行的面向物件軟體設計指南指出,高階模組不應該直接依賴於低階模組。相反,兩者都應該依賴於共同的抽象(以定義良好的介面的形式捕獲)。例如,型別為 Person 的物件(例如 John)不應建立和使用型別為 HondaCivic 的物件,而是 John 應該簡單地承諾 Car 介面,它是 HondaCivic 的抽象基類。這允許 John 在將來輕鬆升級到 Corvette,而無需對 Person 類進行任何更改。John 可以使用 依賴注入 技術用汽車的具體例項(任何汽車)進行“配置”。使用 DIP 導致靈活且可擴充套件的模組,這些模組易於進行單元測試。DIP 簡化了單元測試,因為可以使用依賴注入輕鬆將真實物件替換為模擬物件。
但是,在某些情況下會違反 DIP:(1)在使用單例模式時,以及(2)在丟擲異常時!單例模式破壞了 DIP,因為它在訪問靜態 instance() 函式時強制使用具體類名。應該在呼叫函式或建構函式時將單例作為引數傳遞。在處理 C++ 中的異常時,也會出現類似的情況。C++ 中的 throw 子句需要一個具體型別名(類)來引發異常。例如,
throw MyConcreteException("Big Bang!");
任何丟擲類似異常的模組都會立即導致違反 DIP。自然,這樣的模組更難進行單元測試,因為無法輕鬆地將真實異常物件替換為模擬異常物件。像下面這樣的解決方案慘敗了,因為 C++ 中的 throw 使用靜態型別並且對多型性一無所知。
struct ExceptionBase { };
struct ExceptionDerived : ExceptionBase { };
void foo(ExceptionBase& e)
{
throw e; // Uses static type of e while rasing an exception.
}
int main (void)
{
ExceptionDerived e;
try {
foo(e);
}
catch (ExceptionDerived& e) {
// Exception raised in foo does not match this catch.
}
catch (...) {
// Exception raised in foo is caught here.
}
}
多型異常慣用法解決了這個問題。
多型異常慣用法簡單地使用虛擬函式 raise() 將引發異常的任務委託給派生類。
struct ExceptionBase
{
virtual void raise() { throw *this; }
virtual ~ExceptionBase() {}
};
struct ExceptionDerived : ExceptionBase
{
virtual void raise() { throw *this; }
};
void foo(ExceptionBase& e)
{
e.raise(); // Uses dynamic type of e while raising an exception.
}
int main (void)
{
ExceptionDerived e;
try {
foo(e);
}
catch (ExceptionDerived& e) {
// Exception raised in foo now matches this catch.
}
catch (...) {
// not here anymore!
}
}
throw 語句已移至虛擬函式中。在函式 foo 中呼叫的 raise 函式是多型的,它根據作為引數傳遞的內容(依賴注入)在 ExceptionBase 或 ExceptionDerived 類中選擇實現。*this 的型別顯然在編譯時已知,這會導致引發多型異常。這種慣用法的結構與 虛擬建構函式 慣用法非常相似。
傳播多型異常
通常,異常會在多個 catch 語句中進行處理,以便在程式/庫的不同層中對它進行不同的處理。在這種情況下,較早的 catch 塊需要重新丟擲異常,以便任何外部 catch 塊(如果有)都可以採取必要的措施。當涉及多型異常時,內部 catch 塊可能會在將異常傳遞給堆疊中的 catch 塊之前修改異常物件。在這種情況下,必須注意確保傳播原始異常物件。考慮下面看似無害的程式,它沒有做到這一點。
try {
foo(e); // throws an instance of ExceptionDerived as before.
}
catch (ExceptionBase& e) // Note the base class. Exception is caught polymorphically.
{
// Handle the exception. May modify the original exception object.
throw e; // Warning! Object slicing is underway.
}
throw e 語句不會丟擲原始異常物件。相反,它會丟擲原始物件的切片副本(僅 ExceptionBase 部分),因為它會考慮它前面的表示式的靜態型別。原始異常物件被默默地丟失,並且被轉換為基型別異常物件。堆疊中的 catch 塊無法訪問此 catch 塊擁有的相同資訊。有兩種方法可以解決這個問題。
- 只需使用 throw;(後面沒有任何表示式)。它將重新丟擲原始異常物件。
- 再次使用多型異常慣用法。它將丟擲原始異常物件的副本,因為 raise() 虛擬函式使用 throw *this。
在實踐中應該強烈推薦使用 throw;,因為根據實現,它可能會在異常未處理並且程式轉儲核心時保留原始丟擲位置,從而簡化問題的死後分析。
try {
foo(e); // throws an instance of ExceptionDerived as before.
}
catch (ExceptionBase& e) // Note the base class. Exception is caught polymorphically.
{
// Handle the exception. May modify the original exception object.
// Use only one of the following two.
throw; // Option 1: Original derived exception is thrown.
e.raise(); // Option 2: A copy of the original derived exception object is thrown.
}