重新拋出異常
有時候,您可能會遇到想要捕獲異常的情況,但不希望(或有能力)在捕獲它時完全處理它。這在您想要記錄錯誤,這很很常見,但會將問題傳遞給調用者以實際處理。
當函數可以使用返回代碼時,這很簡單。請考慮以下示例:
Database* createDatabase(std::string filename)
{
try
{
Database *d = new Database(filename);
d->open(); // 假設這會在失敗時拋出一個int異常
return d;
}
catch (int exception)
{
// 數據庫創建失敗
//將錯誤寫入某個全局日誌文件
g_log.logError("Creation of Database failed");
}
return nullptr;
}
在上面的代碼片段中,該函數的任務是創建Database對象,打開數據庫並返回Database對象。在出現問題的情況下(例如傳入錯誤的文件名),異常處理程序會記錄錯誤,然後合理地返回空指針。
現在考慮以下功能:
int getIntValueFromDatabase(Database *d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // 失敗時拋出int異常
}
catch (int exception)
{
// 失敗時拋出int異常
g_log.logError("doSomethingImportant failed");
// 但是,我們實際上沒有處理此錯誤
// 那麼我們在這做什麼?
}
}
在此函數成功的情況下,它返回一個整數值 - 任何整數值都可以是有效值。
但是getIntValue()出現問題的情況呢?在這種情況下,getIntValue()將拋出一個整數異常,該異常將由getIntValueFromDatabase()中的catch塊捕獲,該異常將記錄錯誤。但那麼我們如何告訴調用者getIntValueFromDatabase()出錯了什麼?與頂部示例不同,我們在這裏沒有一個好的返回碼(因爲任何整數返回值都可能是有效的)。
拋出一個新的異常
一個明顯的解決方案是拋出一個新的異常。
int getIntValueFromDatabase(Database *d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // 失敗時拋出int異常
}
catch (int exception)
{
// 將錯誤寫入某個全局日誌文件
g_log.logError("doSomethingImportant failed");
throw 'q'; // 將char異常'q'拋出堆棧,由getIntValueFromDatabase()的調用者處理
}
}
在上面的示例中,程序從getIntValue()捕獲int異常,記錄錯誤,然後拋出char值爲“q”的新異常。雖然從catch塊中拋出異常似乎很奇怪,但這是允許的。請記住,只有try塊中拋出的異常纔有資格被捕獲。這意味着catch塊中拋出的異常不會被它所在的catch塊捕獲。相反,它將在堆棧中傳播給調用者。
catch塊拋出的異常可以是任何類型的異常 - 它不需要與剛剛捕獲的異常類型相同。
重新拋出異常(錯誤的方式)
另一種選擇是重新拋出相同的異常。一種方法是這樣做:
int getIntValueFromDatabase(Database *d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // 失敗時拋出int異常
}
catch (int exception)
{
// 將錯誤寫入某個全局日誌文件
g_log.logError("doSomethingImportant failed");
throw exception;
}
}
雖然這有效,但這種方法有一些缺點。首先,這不會拋出與捕獲的異常完全相同的異常 - 而是拋出變量異常的複製初始化副本。儘管編譯器可以自由地刪除副本,但它可能不會,因此這可能會降低性能。
但重要的是,考慮以下情況會發生什麼:
int getIntValueFromDatabase(Database *d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // 失敗時拋出派生異常
}
catch (Base &exception)
{
// 將錯誤寫入某個全局日誌文件
g_log.logError("doSomethingImportant failed");
throw exception; // 危險:這會拋出一個Base對象,而不是Derived對象
}
}
在這種情況下,getIntValue()拋出Derived對象,但catch塊正在捕獲Base引用。這很好,因爲我們知道我們可以對Derived對象有一個Base引用。但是,當我們拋出異常時,拋出的異常是從變量異常中複製初始化的。變量異常具有類型Base,因此複製初始化異常也具有類型Base(不是Derived!)。換句話說,我們的Derived()對象已被切片!
您可以在以下程序中看到:
#include <iostream>
class Base
{
public:
Base() {}
virtual void print() { std::cout << "Base"; }
};
class Derived: public Base
{
public:
Derived() {}
virtual void print() { std::cout << "Derived"; }
};
int main()
{
try
{
try
{
throw Derived();
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << "\n";
throw b; // the Derived object gets sliced here
}
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << "\n";
}
return 0;
}
這打印:
Caught Base b, which is actually a Derived
Caught Base b, which is actually a Base
第二行表明Base實際上是Base而不是Derived的事實證明Derived對象是切片的。
重新拋出異常(正確的方式)
幸運的是,C ++提供了一種方法來重新拋出與剛剛捕獲的異常完全相同的異常。爲此,只需在catch塊中使用throw關鍵字(沒有關聯的變量),如下所示:
#include <iostream>
class Base
{
public:
Base() {}
virtual void print() { std::cout << "Base"; }
};
class Derived: public Base
{
public:
Derived() {}
virtual void print() { std::cout << "Derived"; }
};
int main()
{
try
{
try
{
throw Derived();
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << "\n";
throw; // 注意:我們現在正在重新拋出這個對象
}
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << "\n";
}
return 0;
}
這打印:
Caught Base b, which is actually a Derived
Caught Base b, which is actually a Derived
這個throw關鍵字似乎沒有特別拋出任何東西,實際上會重新拋出剛捕獲的完全相同的異常。沒有製作副本,這意味着我們不必擔心性能下降副本或切片。
如果需要重新拋出異常,則此方法應優先於替代方法。
規則:當重新拋出相同的異常時,請單獨使用throw關鍵字。