公開日:2007年9月26日
独立行政法人情報処理推進機構
セキュリティセンター
本ページの情報は2007年9月時点のものです。
記載の資料は資料公開当時のもので、現在は公開されていないものも含みます。
ソフトウェアの中で発生するエラーは多岐にわたる。エラーには、代替策をとれば問題なく先へ進めるものや、プログラム自身が問題で修復できるものもある一方で、別のコンピュータへ役割を引き継いだり、人間の介入を待たなくてはならないものまで多種多様である。発生の「深さ」も、デバイスドライバ、オペレーティングシステム、データベース、ミドルウェア、ビジネスロジック、ユーザインタフェース等のあらゆる「階層」にわたる。
これらのエラーに適切に対処すること(エラーハンドリング)は非常に重要である。発生を見過ごしていたり対処が不適切であると、プログラムは想定外の振る舞いをしかねない。これらは悪くすると、機械類の重大な誤作動、システムのサービス不能、データ破壊、情報漏えい等の結果を生む。
エラーハンドリングは、ソースコードの中で大きな比率を占める存在である。
例えば、下位のモジュール(関数)を利用した次の図のような3ステップからなる処理を行う必要があるとする。
各ステップはどれも失敗するおそれがある。そのため、プログラマが記述するロジックは、実際には各ステップの失敗を考慮したものになる。(下図)
このようにして、プログラムの中は多くのエラーハンドリングのコードで占められてゆく。
ソフトウェアを構築する際には、起こりうるエラーをひととおり想定し、それらに対処するコードを書く必要がある。
エラーの想定は、要件定義から詳細設計に至る工程のどれにおいても行う必要がある。システムの機能性、構造、仕様を決めてゆく過程で、そこで起こりうるエラーも段階的に明らかになってくるからである。
その際、エラーへの対処方法を立案するのであるが、対処方法は概ね次の9つのパターンに分類できる(詳しくは後述)。
エラーハンドリングには、一連の流れ(検出→伝達→対処)がある。
対処すべきエラーは、その発生が的確に検出されなくてはならない。
古典的なC言語用ライブラリにおいては、関数の戻り値でエラーを表していた。関数の戻り値をチェックしないコーディングがしばしば行われたため、エラーの発生が見逃されがちであるという問題があった。
C++においては「例外(exception)」が導入された。ランタイムライブラリ等はエラーを例外の発生によって通知するようになり、プログラマは例外の発生を無視できなくなった。
例外を捕捉する際に注意すべきことは、明らかに発生が想定できるもの以外は捕捉しないということである。プログラムの実行が強制終了させられるのを避けるつもりで次のようなコードを書いてしまうと、すべての例外がマスクされ、本来対処すべきエラーの発生に気づかないおそれがある。(catch(...) はすべての例外を捉える際のC++の記法である。)
try {
(例外を発生させるかもしれない処理)
} catch (...) {
// 何もしない
}
悪い例
なお、C++のデストラクタの中ではこのような記述は必要になる。
検出されたエラーは、その事象への対処を担当するモジュールへ伝達されねばならない。伝達の手段には、関数の戻り値、関数が書き込む構造体のフィールド、C++の例外等がありうる。
エラーの対処方法は多種多様である。エラーへの対処のバリエーションには、概ね下記の9つのパターンが考えられる。:
エラーへの対処とともに行うべき重要なことに、リソースの解放がある。
プログラムは必要に応じて、ヒープメモリのブロック、データベース接続、ファイルディスクリプタ、プールされたスレッド等の各種リソースを動的に割り当てて利用する。エラーによる処理の中断ののちもこれらを解放せずにおくと、やがて使えるリソースが枯渇し、プログラムの実行を継続できなくなる。いわゆるメモリリークやリソースリークと呼ばれる現象である。
エラーへの対処時に行うリソース解放は、本稿冒頭の3ステップの処理について言うと、例えば、次のような形になる。
try - catch 構文による例外の捕捉と対処は、次のように最小範囲に絞ることが望ましい。
なぜならば、プログラムの当該地点で想定しているもの以外についても例外を吸収し他へ伝えずにいると、それらへ対処する仕組みをもつかもしれない上位モジュールにエラー発生が伝わらないからである。
C++においては、プログラムの制御が関数から出る際、自動でデストラクタが呼ばれるメカニズムを利用して、リソース解放を行うことができる。ヒープ上に置かれたオブジェクトに関しても、このオブジェクトへの参照を保持するオブジェクトをスタック上に設けることによって同様のことが行える。
ただし、注意すべきことは、デストラクタ内のリソース解放が例外の発生によって中断されてはならない点である。デストラクタの中では、記述するリソース解放処理の各ステップを、例えば、次のような要領でガードする必要がある。
try {
(リソース解放のステップ1)
} catch (...) { /*必要ならログ等へ障害を記録*/ }
try {
(リソース解放のステップ2)
} catch (...) { /*必要ならログ等へ障害を記録*/ }
~
try {
(リソース解放のステップn)
} catch (...) { /*必要ならログ等へ障害を記録*/ }
デストラクタにおいてガードする例
例外処理の仕組みを提供するOSもあるが、言語仕様の上ではC言語に例外処理機構はない。例外処理機構が備わっていないか利用しないことを選択した場合、例えば、次のような関数呼び出しの構造を設けることが考えられる。
それは、業務分野の処理を専門に行う関数を、リソースの割り当て・解放を専門に行う関数が囲い込む形に関数の呼び出し構造を作るというものである。内側の関数ではエラーによる処理の中断が起こりうるが、それらを外側の関数で受け止めてリソース解放を行う。
C、C++およびこれらから影響を受けているいくつかのプログラミング言語について、例外処理とリソース解放のための構文およびランタイム機能の対比を、次ページの表に示す。