C++基礎教程面向對象(學習筆記(78))

重新拋出異常

有時候,您可能會遇到想要捕獲異常的情況,但不希望(或有能力)在捕獲它時完全處理它。這在您想要記錄錯誤,這很很常見,但會將問題傳遞給調用者以實際處理。

當函數可以使用返回代碼時,這很簡單。請考慮以下示例:

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關鍵字。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章