[翻訳]エラーと例外の取り扱い(Boost community の contributed article)


この記事は、Boost コミュニティー Error and Exception Handling の私家翻訳です。

ガイドライン

例外はいつ使用するべきか

単純に言うと「セマンティックとパフォーマンス特性が許すならばいつでも」となります。

よく引用されるガイドラインに、「これは例外的(予期しない)状況か?」と問う、というものがあります。これは一見良さそうに見えますが、通常は間違いです。問題なのは、ある人にとって「例外的」であることが、他の人にとっては「予期する」であるということです。用語を文字通り解釈していくと、区別は蒸発し、ガイドラインは無くなってしまいます。結局のところ、異常系をチェックするということは、ある面ではそれが起こることを予期しているか、もしくはそのチェックは無駄なコードだということです。

もう少し適切な問いの一つは「ここでスタック巻き戻しをしたいか?」です。なぜなら、例外ハンドリングはメインラインコードよりも顕著に遅いからです。「ここでスタック巻き戻しを許容できるか?」とも言えます。例えば、あるデスクトップアプリケーションが計算時間の長い処理をしているとき、ユーザーがキャンセルボタンを押したかどうかを周期的にチェックするとしましょう。例外を投げることで、操作を正常にキャンセルさせることができるでしょう。一方で、この計算の内部ループの中で例外を投げて受け取ることは、顕著なパフォーマンスへのインパクトがあるため、おそらく適切ではないでしょう。上記のガイドラインは一片の真実を含んでいます。タイムクリティカルなコードでは、例外を投げることは、ルールではなく「例外的」にするべきです。

例外クラスはどのように設計するべきか?

1. std::exception クラスから派生させる

仮想テーブルのコストを許容できないような極めて稀なケースを除き、std::exception は例外クラスの最も妥当な基底クラスであり、ユニバーサルに使うことができます。プログラマーは catch(...) に頼ることなく「すべて」の例外をキャッチすることができます。
catch(...) については後の章を参照。

2. 仮想継承を使う

これは Andrew Koenig 氏による考察です。例外基底クラスを仮想継承することで、複数の基底クラスを継承した例外クラスを投げたときに、catch側における曖昧さの問題を防ぐことができます。

    #include <iostream>
    struct my_exc1 : std::exception { char const* what() const throw(); };
    struct my_exc2 : std::exception { char const* what() const throw(); };
    struct your_exc3 : my_exc1, my_exc2 {};

    int main()
    {
       try { throw your_exc3(); }
       catch(std::exception const& e) {}
       catch(...) { std::cout << "whoops!" << std::endl; }
    }

このプログラムは、whoops! と出力します。なぜならC++ランタイムは、例外インスタンスを最初の catch 節にマッチすることができないからです。

3. std::string オブジェクトなど、メンバーや基底クラスのコピーコンストラクターが例外を投げるデータを埋め込まない

例外を投げたポイントで直接 std::terminate() を引き起こすことがあります。同様に、通常のコンストラクターが例外を投げるメンバーや基底クラスを使うのはおすすめできません。なぜならば、プログラムにとって必ずしも致命的ではありませんが、例えば次のような throw 式で意図したものとは異なる例外を受け取る可能性があるからです。

throw some_exception();

例外がコピーされるときに string オブジェクトがコピーされるのを防ぐ方法はいくつかあります。例えば、例外オブジェクトには固定長のバッファを埋め込んだり、参照カウント方式で管理された文字列オブジェクトを使うことです。しかしながら、これらのアプローチを試す前に次の点を検討してください。

4. 本当にメッセージをフォーマットする必要があるならば、what() メッセージはオンデマンドでフォーマットする

例外のエラーメッセージをフォーマットすることは、通常はメモリを多く消費する操作であり、これは例外を発生させる可能性があります。これは、スタックの巻き戻しが起こり、いくつかのリソースが解放されただろう後に行うのが最適な操作です。このケースでは、what() 関数を catch(...) ブロックで保護することで、フォーマットが出す例外をフォールバックすることが良い考えです。

5. what() メッセージを気にしすぎない

プログラマーの手がかりとなるメッセージがあることは良いことですが、例外が投げられた時点で、ユーザーにとって理解できるエラーメッセージを生成することは望み薄です。特に、国際化対応は例外クラスの実装者のスコープを超えています。Peter Dimov は、「what() 文字列の正しい使い方は、エラーメッセージフォーマッターのテーブルのキーとすることである」という卓見を披露しています。標準ライブラリから投げられる例外に、標準化された what() 文字列があればの話ですが……。

6. 例外クラスの public インターフェースでエラーの原因についての情報を開示する

what() メッセージに懲ることは、ユーザーにとって一貫性のあるメッセージを生成するために誰かが必要とする情報を開示することを怠っている印かもしれません。例えば、例外が数値範囲エラーを報告するとき、エラー報告コードがインテリジェントに何かを行うには、その例外のパブリックインターフェースから問題の数値を取得できることが重要です。もしその数値が what() 文字列のテキスト表現としてしか開示されていないなら、あなたはダム出力ではなくそれ以上のこと(例えば減算とか)をしたいプログラマーに大変な手間をかけさせることになります。

7. できるだけ二重解放の影響を受けないようにする

残念ながら、いくつかのコンパイラーは例外オブジェクトの二重解放を起こすことがあります。これが無害になるようにする(例えば、delete したポインターをヌルにする)ことができれば、よりロバストになります。

プログラマーエラーはどうするか?

私は開発者として、使っているライブラリの前提条件に違反したとき、スタック巻き戻しをしたくありません。私がほしいのはコアダンプかそれと同等のもの、問題が検出された正にその瞬間のプログラムの状態を調査する方法です。それは時には assert() のようなものかもしれません。

クライアントのほとんどあらゆる不正な使用に耐える復元力のある API が必要になることも時にはありますが、このアプローチには通常は多大なコストが掛かります。例えば、クライアントが使用しているそれぞれのオブジェクトを、有効性がチェックできるように追跡する必要が通常あります。この類の保護が必要な場合、通常は単純なAPIの上にかぶせたレイヤーとして提供することができます。ただし、中途半端には気を付けてください。全部ではなく一部の不正操作にしかレジリエンスを約束してくれないAPIは、災のもとです。クライアントは、保護に依存し始め、インターフェースが保護しない部分までカバーすることを期待するようになります。

Windows 開発者への注意

残念なことに、ほとんどの Windows コンパイラーが使うネイティブ例外ハンドリングは assert() を使用したときに例外を投げてしまいます。これは他のプログラマーエラー(例えばセグメンテーションフォルトや0除算エラー)にも当てはまります。これについての問題の一つは、JIT デバッグを使用するときです。catch(...) は C++ 例外ではないものもキャッチしてしまうため、デバッガーが起動する前に余計な例外巻き戻しをしてしまいます。幸い、次のおまじないを使う回避策があります。

extern "C" void straight_to_debugger(unsigned int, EXCEPTION_POINTERS*)
{
    throw;
}
extern "C" void (*old_translator)(unsigned, EXCEPTION_POINTERS*)
         = _set_se_translator(straight_to_debugger);

この方法は、SEH が catch ブロック(または catch ブロックの中で呼ばれた関数)から発生した場合には役立ちませんが、JITマスキング問題のほとんどを解消できます。

例外をどのように処理するべきか?

例外を処理する最良の方法は「処理しない」ことであることがしばしばあります。例外を流れるに任せ、デストラクターでクリーンアップさせれば、コードはクリーンになります。

catch(...) は可能な限り使わない

残念ながら、Windows 以外のOSも非C++「例外」(例えばスレッドのキャンセル)をC++例外処理機構にねじ込むことがあります。そして、上で示したような回避策が存在しない場合がしばしばあります。その結果、catch(...) は、適切な位置から投げられたC++例外のようにリカバリーすることができない点で予期しないシステム通知を発生させることがあり、巻き戻しの過程でプログラムが確実に有効な手順を実行したという、デストラクターと catch ブロックの通常安全な仮定を無効にします。

私はニュースグループの長い議論の後、 Hillel Y. Sims 氏に指摘されたこの点をしぶしぶ受け入れました。すべてのOSが「修正」されるまでは、すべての例外は std::exception から派生させ、catch(std::exception&)catch(...) の前に置くことでみんなが幸せになれます。

場合によっては、OSやプラットフォームの設計によって生じる悪い相互作用にもかかわらず、catch(...) は依然として最適なパターンです。もし例外の種類にまったく手がかりがなく、スタック巻き戻しを停止することが本当に必要な場合は、です。これが起きることが明らかな場所の一つは、言語同士の境界です。