スローから処理へ
前のページでは、何か問題が起きたときに例外を throw(スロー)する方法を学びました。スローは話の半分にすぎません。捕捉されない例外は std::terminate を呼び出し、プログラムをクラッシュさせます。try/catch 文は、スローされたものを_処理_して実行を続ける手段です。
形はシンプルです。危険なコードを try ブロックに入れ、その後に特定のエラー型に反応する 1 つ以上の catch ブロックを続けます。try ブロックが問題なく実行されれば、すべての catch はスキップされます。何かがスローされた瞬間、制御は最初に一致する catch へ直行します。
"after" が決して出力されないことに注目してください。throw が発動した瞬間、try ブロックの残りは放棄され、実行は一致する catch の中で再開されます。catch が終わると、プログラムはその下から通常どおり続きます。
const 参照で捕捉する
C++ のエラー処理で最も重要な習慣は、例外を値ではなく const 参照で捕捉することです。
値で捕捉すると例外がコピーされ、さらに悪いことに_スライシング_されます。標準例外は階層を成しており(runtime_error と logic_error はどちらも std::exception から派生)、派生例外を基底の値として捕捉すると派生部分が切り落とされます。参照で捕捉すれば、オブジェクトはそのまま多態的に保たれます。
ここでは out_of_range をスローしていますが、const exception& として捕捉しています。out_of_range は exception から派生しているので基底クラスのハンドラが一致し、参照であるため e.what() は依然として本当のメッセージを返します。もし catch (exception e)(値で捕捉)と書いていたら、オブジェクトはただの exception にスライスされ、具体的なメッセージを失っていたかもしれません。
複数の catch ブロック
1 つの try には、それぞれ異なる例外型に対応する複数の catch ブロックを続けられます。C++ は上から下へ試し、_最初に_一致したものを実行します。そのため、最も具体的なものから最も一般的なものへと並べてください。
invalid_argument は exception より具体的なので、先に来なければなりません。もし順序を逆にして catch (const exception&) を上に置くと、_すべての_例外を飲み込んでしまい、その下の invalid_argument ハンドラは決して実行されないデッドコードになります。多くのコンパイラはこれを警告しますが、言語が止めてくれるわけではありません。
catch (...) と再スロー
想定外のあらゆる事態に備える安全網が欲しいときがあります。キャッチオールのハンドラ catch (...) は、std::exception から派生しないものを含め、あらゆる例外型に一致します(誰かが throw 42; や throw "oops"; と書けます)。
難点は、オブジェクトが得られないことです。調べるための e がありません。そのため catch (...) は最後の手段として使うのが最適です。_何か_が失敗したことをログに記録するか、後始末をして再スローするのに使います。
現在の例外を再スローする(ローカルな後始末やログ出力の後、外側のハンドラへ渡す)には、オペランドなしの素の throw; を使います。これは、スライスされたコピーを再スローしてしまう throw e; とは異なり、元の例外(その本当の型とメッセージ)を保ちます。
内側のハンドラがログを記録して再スローし、main の外側のハンドラがそれに対処します。これには素の throw; を使い、決して throw e; を使わないでください。
スタックの巻き戻しと RAII
例外が try ブロックの外へ伝播するとき、C++ はスタックの巻き戻し(stack unwinding)を行います。throw と一致する catch の間にあるすべてのローカルオブジェクトのデストラクタが、構築とは逆の順序で呼ばれます。これこそが例外を安全にするものです。スタックオブジェクトが保持するリソースは自動的に解放されます。
まさにこのため、リソースは手動の new/delete ではなく RAII 型(std::vector、std::string、スマートポインタなど)で保持すべきです。例外が手動の確保をまたいだときに何が起きるか見てみましょう。
void leaky() {
int* buffer = new int[1000];
mightThrow(); // これがスローすると、次の行は決して実行されない…
delete[] buffer; // …そして buffer がリークする
}
throw が delete[] を飛び越えるため、メモリが失われます。スマートポインタはこれをただで解決します。そのデストラクタが巻き戻しの最中に実行されるからです。
void safe() {
auto buffer = std::make_unique<int[]>(1000);
mightThrow(); // これがスローしても、buffer のデストラクタがメモリを解放してくれる
} // 手動の delete なし、リークなし、例外経路でも
要点:何かを delete するためだけに例外を catch しようとしないでください。後始末はデストラクタに任せ、catch はどう回復するかの判断のために取っておきましょう。
よくある間違いと落とし穴
いくつかの罠が何度も現れます。
例外を通常の制御フローに使わないでください。 スローと巻き戻しは単純な if よりはるかに遅いです。例外は本当に例外的なエラー状況のために取っておき、「ユーザーが空文字列を入力した」ような場合には使わないでください。
空の catch ブロックはバグを隠します。 エラーを黙らせるために catch (...) {} と書くと、失敗が痕跡もなく消えます。最低でも問題をログに記録してください。通常は再スローするか、きちんと処理すべきです。
スローするデストラクタは危険です。 デストラクタがスタックの巻き戻し_中_に(別の例外がすでに進行中のときに)スローすると、プログラムは std::terminate を呼び出します。現代の C++ ではデストラクタは暗黙的に noexcept です。決して例外をデストラクタから逃がさないでください。
catch は try がカバーするものしか見ません。 try に入る_前_にスローされた例外や、その中の呼び出し経路にない別の関数でスローされた例外は、ここでは捕捉されません。catch は、自身の try ブロック内で(直接、またはそこから呼ばれる関数で)実行されるコードだけを保護します。
次へ:未定義動作
例外は、C++ が何か問題が起きたことを伝える_定義された_手段です。スローして、捕捉すれば、動作は予測可能です。しかし C++ には、言語が何の保証もしない、より暗い一角もあります。ダングリングポインタの参照外し、配列の末尾を越えた読み取り、符号付き整数のオーバーフローなどです。次のページでは未定義動作を扱います。何がそれを引き起こすのか、なぜ破滅的に動かなくなる直前まで「動いている」ように見えることがあるのか、そしてそれをコードから締め出す方法を説明します。
よくある質問
C++ で try-catch はどのように動作しますか?
スローする可能性のあるコードを try { } ブロックの中に入れます。例外がスローされると、プログラムは try ブロックの残りの実行を止め、最初に一致する catch ブロックへ飛び、そこでエラーを処理します。何もスローされなければ、catch ブロックは完全にスキップされます。
C++ で例外を const 参照で捕捉すべきなのはなぜですか?
参照で捕捉する(catch (const std::exception& e))と、例外オブジェクトのコピーを避けられ、何より多態性が保たれます。そのため、基底型として捕捉された派生例外でも正しい what() が呼ばれます。値で捕捉する(catch (std::exception e))と派生部分が切り落とされ(スライシング)、情報を失うおそれがあります。
C++ であらゆる例外を捕捉するにはどうすればよいですか?
catch (...) を使います。省略記号は型に関係なくあらゆる例外を捕捉します。最後の手段として便利なハンドラですが、調べるためのオブジェクトが得られないため、具体的な catch ブロックの後に置き、主にログ出力や再スローに使ってください。