使用標準例外


在〈捕捉自訂例外〉中自訂了例外類別,實際上,C++ 標準程式庫在 exception 標頭定義了基礎類別 exception 與一些處理例外的函式,而 stdexcept 標頭中定義了一系列繼承自 exception 的例外類別:

logic_error
    invalid_argument
    domain_error
    length_error
    out_of_range
    future_error(C++11)
bad_optional_access(C++17)
runtime_error
    range_error
    overflow_error
    underflow_error
    regex_error(C++11)
    system_error(C++11)
        ios_base::failure(C++11)
        filesystem::filesystem_error(C++17)
    tx_exception(TM TS)
    nonexistent_local_time(C++20)
    ambiguous_local_time(C++20)
    format_error(C++20)
bad_typeid
bad_cast
    bad_any_cast(C++17)
bad_weak_ptr(C++11)
bad_function_call(C++11)
bad_alloc
    bad_array_new_length(C++11)
bad_exception
ios_base::failure(until C++11)
bad_variant_access(C++17)

這份清單來自〈std::exception〉,那麼該選用哪個呢?〈捕捉自訂例外〉中自訂了 InvalidArgument,似乎可以用 invalid_argument 來取代,那麼 Insufficient 呢?看來沒有對應的類別,那該繼承哪個來自訂例外類別呢?

例外若被 catch 捕捉,只要 catch 處理後沒有拋出例外,後續的流程是可以繼續的,然而,有些例外就算被 catch 捕捉了,最好是別再繼續流程,最多就是留下日誌(logging),然後令程式崩潰,因為這類例外最好的處理方式,是找出引發例外的程式碼,直接修正程式碼,避免重新執行程式再度拋出例外。

例如,記憶體配置方面的例外 bad_alloc、轉型方面的例外 bad_cast 等,這些就該是只留下日誌、令程式停止,修正程式碼,而不是在執行時期嘗試回復程式的執行流程,在以上例外列表除了 logic_error 與其子類別之外,其他第一層或其下子類的例外,都是屬於這類例外,如果想自訂這類例外,建議繼承 runtime_error

另外有些例外,是屬於商務邏輯上的錯誤範範,例如餘額不足,其實是商務邏輯上的考量,這類錯誤可以繼承 logic_error,該類別或其子類實例被拋出,是可以捕捉後嘗試回復執行流程,例如顯示餘額不足後,重新請使用者輸入提領金額。

(是執行時期錯誤還是商務邏輯上的錯誤,有時不見得那麼容易分辨,例如,同樣是標準程式庫提供的例外類別,有些語言會將引數錯誤視為執行時期錯誤,然而 C++ 是歸類在邏輯錯誤。)

就〈捕捉自訂例外〉中的 Insufficient,可以算是商務邏輯上的錯誤,可以繼承標準程式庫的 logic_error 來自訂:

class Insufficient : public logic_error {
    int balance;

public:
    explicit Insufficient(const string &message, int balance) 
                : logic_error(message), balance(balance) {}

    int getBalance() {
        return balance;
    }
};

withdrawdeposit 可以改為:

void Account::deposit(double amount) {
    if(amount <= 0) {
        throw invalid_argument("必須是正數");
    }

    this->balance += amount;
}

void Account::withdraw(double amount) {
    if(amount <= 0) {
        throw invalid_argument("必須是正數");
    }

    if(amount > this->balance) {
        throw Insufficient("餘額不足", this->balance);
    }
    this->balance -= amount;
}

執行時可以如下撰寫:

Account acct = {"123-456-789", "Justin Lin", 1000};
cout << acct.to_string() << endl;

try {
    acct.withdraw(10200);
    acct.deposit(-500);
}
catch(invalid_argument &ex) {
    cout << "引數錯誤:" << ex.what() << endl;
}
catch(Insufficient &ex) {
    cout << "帳號錯誤:"  << endl
         << "\t" << ex.what() << endl
         << "\t餘額 " << ex.getBalance() << endl;
}