例外処理
例外処理（れいがいしょり）とは、プログラムの上位の処理から呼び出されている下位の処理で継続不能、または継続すれば支障をきたす異常事態に陥ったとき、制御を呼び出し元の上位の処理に返し安全な状態になるよう回復処理をすること。その際に発生した異常のことを例外と呼ぶ。
継続不能や継続すると問題になる様な状態としては、次のようなものが挙げられる。あるコード内を実行中の失敗が、メモリリーク、格納データの不整合、不正な出力などの有害な効果を生じないとき、そのコード片は例外安全であると言う。例外安全なコードは例外が発生したとしてもそのコードが備える不変条件を満たさなければならない。例外安全性にはいくつかのレベルがある:
幾つかのプログラミング言語では組み込みの例外処理機能を用意している。例えばAda、C++、Java、Scala、C#、JavaScript、OCamlがそうである。これらの言語では専用の言語機能によってプログラマが例外処理を記述する手間を軽減している。
例外が発生したことを見落として正常時の動作を継続してしまうと、より深刻・致命的な異常を招くおそれがある。それを避けるには例外が発生したことのチェックを綿密に行い、例外が検出された場合には適切な事後処理を行う他ない。しかし、大規模なプログラムではこのようなチェックは膨大なものとなり、本来目的としている正常時の処理よりも多くの記述を必要とする場合すらある。
そこで、これらの言語では例外の発生チェックをほぼ自動化している。例外が発生すると現在の処理を中断する。発生した例外の事後処理を担当できるハンドラを探して次々にコールスタック（関数呼び出し）を遡り、適切なハンドラを見つけるとそれに事後処理を任せる。これにより、遡る途中にあったこの例外を処理する能力を持たない処理は自動的に中断されることになる。
Schemeでは言語レベルでの例外処理を持たないが、これは継続が存在するため例外をライブラリレベルで実現できるからである（標準仕様であるSRFI-34で定義されている）。void Function0(void) throw( int, char const*, std::exception ) // (2)
void Function1(void)
void Function2(void)
try
catch (...)
codice_1ブロック中で呼び出した関数codice_2が(1)のcodice_3を実行すると、codice_4のcodice_5文へと制御が移る。C++では後発の言語とは異なり、std::exceptionあるいはその派生型以外の型の値でもcodice_3で投げることができ、(3)(4)(5)の様に型に対応したcodice_5文で捕捉することができる。なお、(1)では例示のため複数のcodice_3を書いているが、実際には1個目のcodice_3を実行した時点でcodice_5文に移動する。
例外構文には例外の型を分かり易くするため(2)の例外指定という構文が存在するが省略可能であり、実際は例外を投げないことを示すため以外に使われていない。C++11では、例外指定は非推奨機能 (deprecated) となり、代わりにcodice_11が標準化された。
C++の特徴的な構文として、(6)の省略子を用いたcatchが存在する。あらゆる例外を捕捉可能であり、他のcodice_5が取りこぼした例外でも捕まえる必要がある場合に用いる。値を指定しないcodice_3を捕まえられるのも省略子を用いたcodice_5だけである。また、Microsoft Visual C++といった一部の処理系では、コンパイラオプションの指定によりC++例外だけでなくOSが投げた構造化例外も省略子を用いたcodice_5で捕捉できる。codice_5文の中では(7)のように引数の無いcodice_3を用いた場合、例外の再送を意味する。省略子を用いたcodice_5の場合は例外情報を判断できないため必須であるが、省略子を使わないcodice_5でも派生型の例外を基底型で受け取ってしまった場合のスライシングを防ぐために必要となる。
tryブロックは(8)のように関数全体に適用することもできる。これをfunction-try-blockと言う。catch文では局所変数を参照できず引数だけしか参照できないが、コンストラクタの初期化リストで発生した例外やデストラクタから投げられた例外はこの書き方でしか捕捉することができない。
例外処理構文を最初に実装したのはAdaであるが、C++の例外処理構文はJavaやJavaScript、C#など多くの後発言語の規範となった。Javaでは例外はクラスとして実装する。例外を「投げる(throw)」メソッドはcodice_20のように指定する。Javaプラットフォーム上でJava言語を使用し、発生する例外がcodice_21を継承しているが、しかしjava.lang.RuntimeExcptionを継承していない場合、try/catch文で例外処理を明示的に記述するか、メソッドにthrowsを追加する必要がある。ただし、Javaプラットフォーム上で動く言語でも、GroovyやScalaなど、Java言語以外の多くは、RuntimeException以外の例外に対して必ずしも明示的に記述しなくても良くなっている。初期のJava（JDK1.0）では、I/O処理など他の手段では例外の発生を回避することができない種類の例外に対してはRuntimeExceptionを継承させないという設計思想になっていた。value := "式であるため戻り値が存在する"
言語機能としては例外処理構文が存在しないが別途例外処理を備える言語も存在する。Smalltalkは言語機能として制御構文が殆どない。このため例外処理構文もブロックを組み合わせたメッセージ式として記述するようになっている。言語機能ではないため極めて柔軟性がありブロックの戻り値を変更したり例外が発生した式の途中から復帰したりなど様々な制御が可能になっている。
例外処理の過程では処理の流れが通常の制御とは大きく変化することとなるが、これを（エラー処理以外の目的で、正常系において）積極的に利用することは、アンチパターンとされることもある。
一方、RubyやPythonでは、イテレータが終端に達するという、無限ループでなければ必ず発生する事象により例外が起きることもあるほか、Rubyでは例外処理と関係なく大域脱出を行う制御構造も用意されている。
Smalltalkでも例外はエラー処理以外の通知として使われる。Smalltalkの例外 (Exception) はエラー (Error) と通知 (Notification) からなり、EOFの通知やスレッド間の割り込み終了通知等に使われている。またSmalltalkは値を検索した結果値が見つからなかった場合の戻り値としてcodice_22 (null) を返さない傾向があり、値が見つからなければ例外を投げる。ただし単純な例外処理というパターンがあり、値が見つからない場合でも例外構文を使わず安全に回復する手段を用意している。
例外処理（例外オブジェクト）をサポートしないCなどの言語では、従来から関数（サブルーチン）の戻り値によってその関数（処理）の成否を判定する方法がとられてきた。慣例的に、関数の戻り値を32ビット整数値などで宣言して、関数が成功した場合は0を返し、失敗した場合はエラーコードとして何らかの負数を返すことが多い。さらに簡略化して、成否の結果を真偽値1/0で返すだけにすることもある。戻り値がポインタ型である場合は、成功した場合に有効なポインタすなわち非NULLを返し、失敗した場合に無効なポインタすなわちNULLを返すのが通例である。標準ライブラリや各種APIでは、詳細を伝えるエラーコードを別途codice_23のようなグローバル変数に格納することもある。各エラーコードによって失敗の原因を定義しておき、呼び出し側で原因を判定する。
このような戻り値による処理の成否判定には下記のような問題点がある。
3. に関連する問題として、戻り値が正常系の結果取得に使えないため引数を処理結果の取得用に使い関数インターフェイスおよび呼び出し側のコードが複雑化するという問題がある。
4.の問題では関数の呼び出し結果をいったんローカル変数に格納することなく次の関数引数にそのまま式として渡すようなこともできなくなる。例えば下記のC言語の例では、atof()関数の戻り値が正常系と異常系とで区別がつかない仕様のため、対象フォーマット外の不正な入力があっても検知できず処理を継続してしまう。例外を使わずにこの問題に対処するには、正常系と異常系とを区別できるようにするために、関数の実装およびインターフェイスが複雑化することを許容しなければならない。
一方、文字列から数値への変換に失敗した場合に例外を投げるライブラリを持つ言語では、メインロジックに関係のないコードを挿入することなく、正常系と異常系とを簡潔かつ明確に区別できる。下記はC#による例である。
別の例として、たとえば主記憶領域の容量やファイル容量を取得する関数を設計する際、結果を符号なし整数型 (非負数) の戻り値で返すように決めた場合、戻り値でエラー判定用の値を返すことができない。この場合、codice_23のようなグローバル変数もしくは別途用意した関数引数経由でエラーコードを返して、呼び出し側で判定する必要がある。たとえばWindows APIのGetFileSize()関数は戻り値でファイルサイズを返すが、混合した設計となっており、エラーが発生した場合はcodice_25を返す。しかし戻り値の型はcodice_26つまり符号なし32ビット整数型であるため、実際にはcodice_27が返却される。これは本来正常値としてありうる値であり、異常系との区別が付かないため、エラーによる結果だったかどうかを判定するにはGetLastError()関数の呼び出しが別途必要になっている。なおcodice_28関数は成否を戻り値で、正常系出力を引数で返す設計となっており、GetFileSize()関数の代替として推奨されている。また、C言語において身近な例としてはcodice_29およびcodice_30、codice_31関数があげられる。これらは確保を要求する容量として0を指定したときC言語の規格としてNULLを返して良いと定義されている。このためこれらの関数の戻り値では記憶空間の容量不足か引数に0を指定したか判断できず切り分けのためにcodice_23を調べるか引数を調べる必要がある。C++において同等のnew演算子ではこの点を容量不足のときだけ例外を投げることによって改善している。
言語レベルでの例外処理はこれらの欠点を解消し、エラーを確実に、かつ統一的に処理する目的で導入されたものと言える。
処理中に使用可能な記憶領域が枯渇した場合の回復は比較的単純である。
記憶領域が原因として上記が考えられるがGUIプログラムのイベント処理であれば3以外の状況から回復できる。1,2どちらも現在実行中の処理を中断して処理に使った記憶領域を解放すればプロセスを継続できる可能性がある。特に2の状況であればシステムの記憶領域使用状況が回復を待って再度同じ処理を実行することができる。
CUIの場合でかつ複数のファイルを処理しているような場合1の状況から回復することができる。枯渇が発生した大容量ファイルの処理は無理でもその後の小容量ファイルは処理できる可能性がある。
次にSmalltalkによる回復処理の例を示す:
"ファイルの読み込み失敗を通知する部品"
notifyArea := LabelStringMorph new.
"ファイルを開くための部品"
textBox := TextMorph new.
filePath := TextMorph new.
open := SimpleButtonMorph new.
"ボタンをクリックしたらファイルを読み込んでテキストボックスに表示できるようにする"
open
"ファイルを表示するテキストボックスとファイルの読み込みに必要な各種部品を組み込んだWindowを表示する"
window := SystemWindow new.
window