なぜ例外が存在するのか
前のページでは、enum classを使ってエラー状態に意味のある名前を付けました。これは、関数が想定し、呼び出し側が確認すべき結果には最適です。しかし、一部の失敗は性質が異なります。呼び出しスタックの奥深くにある関数が、ファイルが開けないことや、引数がまったく意味をなさないことを発見しても、その関数にはプログラムがそれにどう対処すべきか見当もつきません。エラーコードを返すやり方は、連鎖するすべての呼び出し側がそれを確認して上に伝えることを忘れなかった場合にのみ機能します。一つでも確認を見落とせば、プログラムはゴミデータを抱えたまま進み続けてしまいます。
例外はこれを解決します。何か問題が起きたら、throwでオブジェクトを投げます。実行は直ちに停止し、スタックが_巻き戻され_(throwとハンドラの間にあるすべてのローカルオブジェクトのデストラクタが実行され)、制御は最も近い一致するcatchへ飛びます。処理されない例外を黙って無視することはできません。何もキャッチしなければ、プログラムはstd::terminateを呼び出して異常終了します。
このページでは、投げる側、つまりエラーオブジェクトそのものに焦点を当てます。次のページでは、try/catchの仕組みを詳しく掘り下げます。
throwとwhat()メッセージ
技術的には、どんな値でもthrowできます。throw 42;やthrow "oops";も合法です。しかし、そうしてはいけません。誰もが従う慣習は、std::exceptionを継承したオブジェクトを投げることです。その基底クラスは、問題を表すconst char*を返す仮想メソッドwhat()を一つだけ宣言しています。慣習に従えば、たった一つのcatch (const std::exception& e)で何でも処理できます。
<stdexcept>ヘッダは、コンストラクタがメッセージを受け取る既製の型を提供します。
what()が、例外を構築したときの文字列をそのまま返すことに注目してください。また、runtime_errorを投げたにもかかわらずconst exception&でキャッチしている点にも注目してください。これが機能するのは、runtime_errorがstd::exceptionである(継承のページで見覚えのある関係)からです。
標準例外の階層
独自の例外型を書く前に、標準ライブラリに合うものがすでにないか確認しましょう。それらはすべてstd::exceptionを継承し、<stdexcept>の中で2つの系統に分かれます。
logic_error— 原理的には実行前に検出できるはずの、プログラムのロジック上のバグ。サブタイプにはinvalid_argument、out_of_range、domain_error、length_errorがあります。runtime_error— 実行時にしか現れず、それ自体はプログラミングミスとは言えない失敗。サブタイプにはrange_error、overflow_error、underflow_errorがあります。
多くのライブラリ関数がこれらをあなたの代わりに投げてくれます。たとえばstd::vector::at()は境界チェックを行い、末尾を越えて読ませる代わりにout_of_rangeを投げます。
このat()はv[9]の安全な対応版です。素のoperator[]は境界チェックを一切行いません。ここでv[9]を読むのは例外ではなく未定義動作です。at()を選ぶことが、静かな破壊をキャッチ可能なエラーに変える方法です。
エラーを表す型を選びましょう。呼び出し側がおかしなものを渡したときはinvalid_argument、インデックスやキーの問題にはout_of_range、「外の世界が自分を裏切った」ときにはruntime_errorです。
独自の例外型を書く
標準の型がどれも合わないとき——追加のデータを付けたい、あるいは自分のエラーだけを狙ってcatchし、それ以外は捕まえたくないとき——std::exception(またはそのサブタイプの一つ)を継承するクラスを定義し、what()をオーバーライドします。std::runtime_errorを継承するのが一番手軽です。すでにメッセージを保持し、what()を代わりに実装してくれるからです。
NetworkErrorはステータスコードを持っているので、ハンドラはそれに反応できます。5xxなら再試行し、4xxなら諦める、といった具合です。ただのエラー文字列ではこうはいきません。カスタム型はさらに、catch (const NetworkError&)でネットワークの問題だけを捕まえ、それ以外はすべてその下のより一般的なハンドラに任せることも可能にします。
もしstd::exceptionから直接(runtime_errorからではなく)継承する場合は、what()を自分でオーバーライドし、基底のシグネチャに合わせてnoexceptを付けることを忘れないでください。
class ParseError : public std::exception {
public:
const char* what() const noexcept override {
return "failed to parse input";
}
};
値で投げ、参照で受け取る
これはC++の例外で最も重要なルールであり、初心者が間違えるところです。オブジェクトは値で投げ、const参照で受け取りましょう。
throw runtime_error("oops"); // 値で投げる - 正しい
catch (const runtime_error& e) { ... } // const参照で受け取る - 正しい
代わりに値で受け取ると——catch (std::exception e)——例外が基底クラスのオブジェクトにコピーされ、派生部分が_スライス(切り落とされ)_します。スライスされた後、e.what()はあなたがオーバーライドしたものではなく基底の実装を呼ぶので、丹念に作り込んだメッセージは消えてしまいます。
try {
throw NetworkError(503, "service unavailable");
} catch (std::exception e) { // 値で受け取る - オブジェクトスライシング!
std::cout << e.what(); // 一般的なメッセージ、status()は失われる
}
参照(&)は本当の動的型を保つので、仮想のwhat()が正しくディスパッチされ、派生クラスのメンバにもアクセスできます。例外は読むだけで変更しないので、constを付けましょう。ポインタを投げては決していけません(throw new runtime_error(...))。受け取った側がそれをdeleteしなければならず、しかもどの実行経路で? それこそが、例外が防ぐはずのリークそのものです。
次は:try-catch
これで、整った例外を作ってthrowし、それぞれの失敗に適した標準型を選べるようになりました。物語のもう半分は、受け取る側です。次のページではtry/catchを余すところなく扱います。複数のcatchブロックを最も具体的なものから最も一般的なものへ並べる方法、すべてを受け取るcatch (...)、裸のthrow;による再スロー、そしてRAII(スマートポインタを思い出してください)がスタックの巻き戻し中にリソースの解放をどう保証するか、です。
よくある質問
C++の例外とは何ですか?
例外とは、現在の関数が自力では処理できないエラーを知らせるオブジェクトです。throwでそれを投げると、スタックが巻き戻され(その途中でローカルオブジェクトが破棄され)、上位にある一致したcatchブロックが処理を引き継ぎます。これにより、問題を検出するコードと、それにどう対処するかを決めるコードを分離できます。
エラーにおけるthrowとreturnの違いは何ですか?
returnの値は呼び出し側が確認しなければならず、それを忘れがちです。すると、プログラムは不正なデータを抱えたまま動き続けてしまいます。投げられた例外は無視できません。誰もキャッチしなければ、プログラムは終了します。例外は本物の失敗(ファイルが開けない、入力が不正など)のためのものです。戻り値は、想定内の「見つからない」ケースを含め、通常の結果には今も適切です。
C++の例外でwhat()メソッドは何をしますか?
std::exceptionを継承したすべてのクラスは、エラーを説明するconst char*を返す仮想メソッドwhat()を提供します。例外をキャッチしたとき、e.what()を呼ぶと、ログに記録したり表示したりできる人間が読めるメッセージが得られます。標準の例外型は、コンストラクタに渡した文字列からこれを設定します。