コンピュータプログラミングでは、例外処理のためのプログラミング言語の仕組みがいくつか存在します。例外という用語は通常、例外的な状況に関する情報を格納するデータ構造を表すために使用されます。制御を移したり、例外を発生させたりする仕組みの1つは、 throwと呼ばれます。例外は、throw されたと言われます。実行はcatchに移されます。
プログラミング言語によって、例外の概念は大きく異なります。例外は、異常で予測不可能なエラー状況を表現して処理するために使用できますが、通常の状況を処理するフロー制御構造としても使用できます。たとえば、Python のイテレータは、イテレータによって生成される項目がこれ以上ないことを示すために StopIteration 例外をスローします。[ 1 ]例外の慣用的な使用法を構成するものについては、多くの言語で意見が分かれています。たとえば、Joshua Bloch は、Java の例外は例外的な状況でのみ使用すべきであると述べていますが、[ 2 ] Kiniry は、Java のjava.io.FileNotFoundExceptionクラスは全く例外的なイベントではないと指摘しています。[ 3 ]同様に、C++ の著者である Bjarne Stroustrup は、C++ の例外はエラー処理のために設計されたため、エラー処理にのみ使用すべきであると述べていますが、[ 4 ] Kiniry は、Ada、C++、Modula-3、ML および OCaml、Python、Ruby などの多くの現代的な言語がフロー制御に例外を使用していると指摘しています。 Eiffel、C#、Common Lisp、 Modula-2などの言語は、例外の使用を制限するために協調的な努力をしてきましたが、これは技術的なレベルではなく社会的なレベルで行われています。[ 3 ]
初期のIBM Fortranコンパイラには、例外条件をテストするためのステートメントがありました。これにはIF ACCUMULATOR OVERFLOW、、、IF QUOTIENT OVERFLOWおよびIF DIVIDE CHECKステートメントが含まれていました。マシン非依存性を考慮して、これらはFORTRAN IVおよびFortran 66規格には含まれませんでした。しかし、Fortran 2003以降では、モジュール内の関数を呼び出すことで数値的な問題をテストすることが可能ですIEEE_EXCEPTIONS。
ソフトウェア例外処理は1960年代と1970年代に開発が続けられました。LISP 1.5 (1958-1961) [ 5 ]ERRORでは、インタープリタやコンパイラが発生させるエラーと同様に、擬似関数によって例外を発生させることができました。例外はERRORSETキーワードでキャッチされ、NILエラーが発生した場合はプログラムを終了したりデバッガに入ったりする代わりに、戻り値を返すようになりました。[ 6 ] PL/Iは1964年頃に独自の例外処理を導入し、割り込みをONユニットで処理できるようにしました。[ 7 ] MacLispはERRSET、とがERRエラー発生だけでなく、非ローカル制御フローにも使用されていることに気づき、2つの新しいキーワードCATCHとを追加しましたTHROW(1972年6月)。[ 8 ]現在一般的に「finally」と呼ばれるクリーンアップ動作は、1970年代半ばから後半にかけてNILUNWIND-PROTECT (New Implementation of LISP)でとして導入されました。[ 9 ]これはその後Common Lispに採用されました。これと同時期にdynamic-wind、クロージャで例外を処理するSchemeがありました。構造化例外処理に関する最初の論文はGoodenough (1975a)とGoodenough (1975b)でした。[ 10 ]例外処理はその後、1980年代以降、多くのプログラミング言語で広く採用されました。
多くのコンピュータ言語には、例外と例外処理のための組み込みの構文サポートがあります。これには、 Ada、BlitzMax、C++、C#、Clojure、COBOL、D、ECMAScript(例:ActionScript、JavaScript)、Eiffel、Java、ML、Object Pascal(例:Delphi、Free Pascal)、PowerBuilder、Objective-C、OCaml、Perl、[ 11 ] PHP(バージョン5以降)、PL/I、PL/SQL、Prolog、Python、REALbasic、Ruby、Scala、Smalltalk、Tcl、Visual Prolog、およびほとんどの.NET言語が含まれます。
構文上の細かな違いを除けば、例外処理のスタイルはごく少数しか使われていません。最も一般的なスタイルでは、例外は、例外オブジェクト(JavaやObject Pascalなど)または特別な拡張可能な列挙型の値(AdaやSMLなど)を指定した特別なステートメント(throwまたは)によって開始されます。例外ハンドラのスコープは、マーカー句(または言語のブロック開始句など)で始まり、最初のハンドラ句( 、、 )の開始で終わります。複数のハンドラ句が続く場合があり、それぞれが処理する例外タイプと例外オブジェクトに使用する名前を指定できます。マイナーなバリエーションとして、一部の言語では、例外のクラスを内部的に処理する単一のハンドラ句を使用します。raisetrybegincatchexceptrescue
また、例外が発生したかどうかに関わらず実行される関連句(finallyまたは)もよく見られます。これは通常、例外処理ブロックの本体内で取得したリソースを解放するために使用されます。注目すべきは、C++はこの構造を提供しておらず、代わりにデストラクタを使用してリソースを解放するリソース取得初期化(RAII)手法を推奨している点です。[ 12 ] Westley WeimerとGeorge Neculaによる2008年の論文によると、Javaの...ブロックの構文はソフトウェアの欠陥の一因となっています。メソッドが3~5個のリソースの取得と解放を処理する必要がある場合、プログラマは可読性の問題から、たとえそれが正しい解決策であっても、十分な数のブロックをネストすることを嫌がるようです。複数のリソースを扱う場合でも単一の...ブロックを使用することは可能ですが、そのためには番兵値を正しく使用する必要があります。番兵値は、この種の問題におけるバグのもう一つの一般的な原因です。[ 13 ] : 8:6–8:7ensuretryfinallytryfinally
elsePythonとRubyでは、ハンドラのスコープの終了までに例外が発生しなかった場合に使用される句()も許可されています。
例外処理コード全体は、Javaスタイルの例外処理構文では、次のようになるでしょう。
import java.io.IOException ; import java.util.Scanner ;try { Scanner stdin = new Scanner ( System . in ); String line = stdin . nextLine ();if ( line.length () == 0 ) { throw new IOException ( "コンソールから読み取った行が空でした!" ) ; }System.out.printf ( " Hello %s!%n", line ) ; System.out.println ( "タスクは正常に実行されました。" ) ; } catch ( IOException e ) { System.out.println ( " Hello ! " ); } catch ( Exception e ) { System.out.printf ( " Error : %s%n" , e.getMessage ( ) ) ; } finally { System.out.println ( "プログラムは終了します。" ) ; }C言語にはtry-catch例外処理はありませんが、エラーチェックには戻り値を使用します。setjmp標準longjmpライブラリ関数を使用して、マクロを介してtry-catch処理を実装できます。[ 14 ]
Perl 5 はtry-catchdieにthrowとを使用します。CPANにはtry-catch セマンティクスを提供するモジュールがあります。[ 15 ]eval{}if($@){}
例外がスローされると、プログラムは例外ハンドラが見つかるまで関数呼び出しのスタックを遡って検索します。一部の言語では、この検索の進行に合わせてスタックを巻き戻す必要があります。つまり、例外fハンドラを含む関数が関数を呼び出し、さらに関数が関数を呼び出し、で例外が発生した場合、関数と関数は終了し、でが処理されます。これは終了セマンティクスと呼ばれます。一方、例外処理メカニズムは例外ハンドラへのエントリ時にスタックを巻き戻さない場合があります[注1 ]。これにより、例外ハンドラは計算を再開、再開、または巻き戻しするオプションを持つことができます。これにより、プログラムはエラーが発生したまったく同じ場所から計算を続行したり(たとえば、以前は存在しなかったファイルが利用可能になった場合)、例外処理メカニズムの上に通知、ログ記録、クエリ、および流動的な変数を実装したりできます(Smalltalkで行われているように)。計算を中断したところから再開できるようにすることを再開セマンティクスと呼びます。HEghEhhgHfE
どちらの決定にも理論的および設計上の根拠がある。1989年から1991年にかけてのC++標準化に関する議論の結果、C++では終了セマンティクスを使用するという決定的な結論に至った。[ 16 ] Bjarne Stroustrupは、 Jim Mitchellのプレゼンテーションを重要なデータポイントとして挙げている。
ジムは20年以上にわたり6つの言語で例外処理を使用しており、ゼロックスのCedar/Mesaシステムの主要な設計者および実装者の1人として、再開セマンティクスの初期の提唱者でした。彼のメッセージは
- 「契約解除は再開よりも望ましい。これは意見の問題ではなく、長年の経験に基づく事実である。再開は魅力的ではあるが、正当な理由とはなり得ない。」
彼はこの主張をいくつかのオペレーティングシステムの経験に基づいて裏付けた。重要な例はCedar/Mesaである。これは再開を好み、使用する人々によって書かれたが、10年間使用した後、50万行のシステムで再開の使用箇所はコンテキスト照会の1つだけになっていた。実際にはそのようなコンテキスト照会には再開は必要なかったため、彼らはそれを削除し、システムのその部分で大幅な速度向上を発見した。再開が使用されたすべてのケースで、10年の間にそれが問題となり、より適切な設計に置き換えられた。基本的に、再開のすべての使用は、別々の抽象化レベルを分離しておくことの失敗を表していた。[ 10 ]
例外処理と再開機能を備えた言語には、条件システムを備えたCommon Lisp、PL/I、Dylan、R、[ 17 ]、およびSmalltalkなどがあります。しかし、新しいプログラミング言語の大部分は C++ に倣い、終了セマンティクスを使用しています。
C++では、std::uncaught_exceptions()現在のスレッドでスロー/再スローされ、まだ一致するブロックに入っていない例外の数をカウントする機能が用意されています。C ++20catchより前は、スタックアンワインディングが発生しているかどうかを判断する別の関数が使用されていました。[ 18 ]std::uncaught_exception()
プログラミング言語における例外処理の実装には、通常、コードジェネレータとコンパイラに付属するランタイムシステムの両方からのかなりのサポートが必要となる。(C++ に例外処理が追加されたことで、オリジナルの C++ コンパイラであるCfrontの有用寿命は終わった。[ 19 ])最も一般的な方式は 2 つある。1 つ目は、動的登録は、例外処理の観点からプログラムの状態に関する構造を継続的に更新するコードを生成します。 [ 20 ]通常、これはスタックフレームレイアウト に新しい要素を追加し、そのフレームに関連付けられた関数またはメソッドで使用可能なハンドラを把握します。例外がスローされると、レイアウト内のポインタがランタイムを適切なハンドラコードに誘導します。このアプローチはスペースの面ではコンパクトですが、フレームの開始と終了時に実行オーバーヘッドが発生します。これは、たとえば、他の多くの言語機能で複雑な生成とランタイムサポートがすでに必要とされていた多くの Ada 実装で一般的に使用されていました。Microsoft の 32 ビット構造化例外処理(SEH) は、このアプローチを別の例外スタックで使用しています。 [ 21 ]動的登録は定義が非常に簡単なので、正当性の証明に適しています。 [ 22 ]
2番目の方式、そして多くの実用レベルのC++コンパイラや64ビット版Microsoft SEHに実装されている方式は、テーブル駆動型アプローチ。これはコンパイル時とリンク時に静的テーブルを作成し例外処理に関してプログラムカウンタの範囲をプログラム状態に関連付けます。 [ 23 ] そして、例外がスローされると、ランタイムシステムはテーブル内の現在の命令位置を検索し、どのハンドラが実行中で、何を行う必要があるかを判断します。このアプローチは、例外がスローされない場合の実行オーバーヘッドを最小限に抑えます。これにはいくらかのスペースが必要ですが、このスペースは、例外が実際にスローされるまでロードまたは再配置されない読み取り専用の特殊目的データセクションに割り当てることができます。 [ 24 ] 例外を処理するコードの位置(メモリ内)は、関数の残りのコードが格納されているメモリ領域内(またはその近く)にある必要はありません。したがって、例外がスローされた場合、必要な例外処理コードをロード/キャッシュする必要がある場合、関数呼び出し[ 25 ]にほぼ匹敵するパフォーマンス低下が発生する可能性があります。ただし、この方式は、例外がスローされない場合、パフォーマンスコストが最小限です。例外的な(つまりまれな) イベントであると想定されているためC++ の例外処理を説明する際に「ゼロ コスト例外」 [注 2 ]という表現が使われることがあります。ランタイム型識別(RTTI) と同様に、例外は C++ のゼロ オーバーヘッド原則は、実行時に例外処理を実装するには、ルックアップ テーブル。 [ 26 ]このため、例外処理 (および RTTI) は多くの C++ コンパイラで無効にすることができ、これはメモリが非常に限られているシステム (組み込みシステムなどに役立つ可能性があります[ 26 ] 。この 2 番目のアプローチは、スレッド セーフティを実現するという点でも優れています。
C++ では任意の型をスローしてキャッチできますが、Java では を拡張する型のみがjava.lang.Throwableスローしてキャッチでき、 にはjava.lang.Throwable2 つの直接の子孫があります。java.lang.Error(合理的なプログラムがキャッチする必要がない深刻な問題を示す)、およびjava.lang.Exception(合理的なプログラムがキャッチして処理したいその他の条件)。は通常、 、、 などjava.lang.Errorのプログラムの範囲を超える非常に深刻な問題のために予約されています。java.lang.OutOfMemoryErrorjava.lang.ThreadDeathjava.lang.AssertionError
他にも定義や実装のスキームが提案されている。メタプログラミングをサポートする言語では、(既に存在するリフレクションのサポート以外に)オーバーヘッドが全くないアプローチが提案されている。[ 27 ]
例外処理に関する別の見解は、契約による設計の原則に基づいており、特にEiffel言語によって支えられています。その考え方は、「正常な」動作と「異常な」動作を正確に定義することで、例外処理のより厳密な基盤を提供することです。具体的には、このアプローチは次の2つの概念に基づいています。
ベルトラン・メイヤーが『オブジェクト指向ソフトウェア構築』で提唱した「安全な例外処理の原則」によれば、例外が発生した際にルーチンが反応できる意味のある方法は2つしかない。
特に、例外を単に無視することは許されません。ブロックは再試行して正常に完了するか、例外を呼び出し元に伝播する必要があります。
以下はEiffel構文で表現された例です。この例では、ルーチンがsend_fast通常はメッセージを送信する最良の方法であると想定していますが、ルーチンが失敗して例外が発生する場合があります。その場合、アルゴリズムは次にsend_slow、失敗頻度の低い を使用します。 がsend_slow失敗した場合、ルーチンsend全体が失敗し、呼び出し元に例外が発生します。
send ( m : MESSAGE ) is -- 可能であれば高速リンク経由で m を送信し、そうでなければ低速リンク経由で送信します。local tried_fast , tried_slow : BOOLEAN do if tried_fast then tried_slow := True send_slow ( m ) else tried_fast := True send_fast ( m ) end rescue if not tried_slow then retry end endブール型のローカル変数は、開始時に False に初期化されます。 send_fastが失敗した場合、本体 (do句) が再度実行され、 が実行されますsend_slow。 の実行がsend_slow失敗した場合、句は(最後の に 句がない)rescueで最後まで実行され、ルーチン全体の実行が失敗します。retryelseif
このアプローチの利点は、「正常」なケースと「異常」なケースを明確に定義できる点です。例外を引き起こす異常なケースとは、ルーチンが契約を履行できない場合を指します。また、役割分担も明確に定義されています。do節(通常の本体)は、ルーチンの契約を達成する、あるいは達成しようとする役割を担います。rescue節は、コンテキストを再構築し、成功する可能性がある場合にプロセスを再開する役割を担いますが、実際の計算は行いません。
Eiffel の例外にはかなり明確な哲学があるものの、Kiniry (2006) はその実装を批判している。「言語定義の一部である例外は INTEGER 値で表現され、開発者定義の例外は STRING 値で表現される。[...] さらに、これらは基本値であってオブジェクトではないため、ヘルパールーチンで表現される以上の固有の意味を持たない。表現のオーバーロードが事実上存在するため、ヘルパールーチンは必然的に万全とは言えない (例えば、同じ値の 2 つの整数を区別することはできない)。」[ 3 ]
C++26では契約のサポートが追加され、以下のように使用されます。[ 28 ]
int f ( const int x ) pre ( x != 1 ) // 事前条件アサーションpost ( r : r == x && r != 2 ) // 事後条件アサーション。r は f の結果オブジェクトの名前です{ contract_assert ( x != 3 ); // アサーション文return x ; }現代のアプリケーションは、例外処理戦略を検討する際に多くの設計上の課題に直面します。特に現代のエンタープライズレベルのアプリケーションでは、例外はプロセス境界やマシン境界を越えることがよくあります。堅牢な例外処理戦略を設計する上で重要なのは、プロセスがソフトウェア部分では経済的に処理できないほど失敗したことを認識することです。[ 29 ]
例外がスローされ、キャッチされない場合(操作的には、適用可能なハンドラが指定されていない場合に例外がスローされます)、キャッチされない例外はランタイムによって処理されます。この処理を行うルーチンは、捕捉されない例外ハンドラ。 [ 30 ] [ 31 ]最も一般的なデフォルトの動作は、プログラムを終了し、エラー メッセージをコンソールに出力することです。通常、例外の文字列表現やスタック トレース。 [ 30 ] [ 32 ] [ 33 ]これは、例外がランタイムに到達する前に例外を捕捉するトップ レベル (アプリケーション レベル) ハンドラ (たとえばイベント ループ) を用意することで回避されることがよくあります。 [ 30 ] [ 34 ]
捕捉されない例外によってプログラムが異常終了する可能性がある場合でも(例外が捕捉されない場合、特に部分的に完了したトランザクションをロールバックしない、またはリソースを解放しないなど、プログラムが正しく動作しない可能性がある)、ランタイムが正しく動作していると仮定すると、プロセスは正常に終了します。これは、ランタイム(プログラムの実行を制御するもの)がプロセスの秩序あるシャットダウンを保証できるためです。
マルチスレッドプログラムでは、スレッド内で捕捉されない例外が発生した場合、プロセス全体ではなく、そのスレッドのみが終了する可能性があります(スレッドレベルのハンドラで捕捉されない例外は、トップレベルのハンドラによって捕捉されます)。これは、例えばサーブレット(独自の別スレッドで実行されている)が終了しても、サーバー全体に影響を与えないサーバーにとって特に重要です。
このデフォルトの未処理例外ハンドラは、グローバルまたはスレッドごとにオーバーライドできます。たとえば、未処理例外の代替ログ記録やエンドユーザーへの報告を提供したり、未処理例外によって終了したスレッドを再起動したりするためにオーバーライドできます。たとえば、Java では、単一のスレッドに対してはThread.setUncaughtExceptionHandler、グローバルには でオーバーライドしますThread.setDefaultUncaughtExceptionHandler。Python では、 を変更することでオーバーライドしますsys.excepthook。
Javaでは、チェック例外の概念が導入されました[ 35 ] [ 36 ]。これは例外の特殊なクラスです。Javaでは、チェック例外とは、またはjava.lang.Throwableを拡張しない例外のことです。メソッドが発生させる可能性のあるチェック例外は、メソッドのシグネチャの一部である必要があります。たとえば、メソッドがをスローする可能性がある場合、メソッドのシグネチャでこの事実を明示的に宣言する必要があります。そうしないと、コンパイル時エラーが発生します。これは、次のように宣言されます(もを使用します)。java.lang.RuntimeExceptionjava.lang.Errorjava.io.IOExceptionjava.util.zip.DataFormatException
import java.io.File ; import java.io.IOException ; import java.util.zip.DataFormatException ;// IOException と DataFormatException がスローされる可能性があることを示しますpublic void operateOnFile ( File f ) throws IOException , DataFormatException { // ... }ハンスペーター・メッセンベックによれば、チェック例外は利便性は劣るものの、より堅牢である。[ 37 ]チェック例外は、コンパイル時に、特定のアプリケーションで実行時に発生する未処理例外の発生率を減らすことができる。
Kiniry は次のように書いています。「Java プログラマーなら誰でも知っているように、try catch典型的な Java アプリケーションのコード量は、チェック例外を持たない他の言語で明示的な仮引数と戻り値のチェックに必要な同等のコード量よりも大きい場合があります。実際、現場の Java プログラマーの間では、チェック例外を扱うことはドキュメントを書くのとほぼ同じくらい不快な作業であるという共通認識があります。そのため、多くのプログラマーはチェック例外を「嫌っている」と報告しています。」[ 3 ] Martin Fowler は次のように書いています。「... 概して、例外は良いものだと思いますが、Java のチェック例外は、その価値よりも面倒です。」[ 38 ] 2006 年現在、主要なプログラミング言語で Java に続いてチェック例外を追加したものはありません。[ 38 ]例えば、C#では例外仕様の宣言は要求も許可もされていません。Eric Gunnerson は次のように投稿しています。[ 39 ] [ 3 ] [ 38 ]
「小規模なプログラムの検証では、例外仕様を要求することで開発者の生産性とコード品質の両方を向上させることができるという結論に至るが、大規模なソフトウェアプロジェクトの経験では、生産性の低下とコード品質の向上はほとんど、あるいは全く見られないことが示唆されている。」
Anders Hejlsberg は、チェック例外に関する 2 つの懸念事項を説明しています。[ 40 ]
X。Y後のバージョンのコードでは、Z新しいコードが以前の使用と互換性がなくなるため、メソッドから例外をスローすることはできません。 チェック例外では、メソッドの呼び出し元はZthrows 句に を追加するか、例外を処理する必要があります。 または は、またはZとして誤って表現される可能性があります。XYこれらの問題を回避するために、プログラマーは宣言を使用してこの機能を回避することに頼っているとHejlsbergは述べています。別の回避策は、 (または)ハンドラーを使用することです。[ 40 ]これは、番組のキャッチフレーズ「Gotta Catch 'Em All! 」にちなんで、キャッチオール例外処理またはポケモン例外処理と呼ばれています。[ 41 ] Javaチュートリアルでは、キャッチオール例外処理は「ハンドラーが意図していない例外」をキャッチする可能性があるため推奨されていません。[ 42 ]さらに推奨されない回避策は、すべての例外をサブクラスにすることです。[ 43 ]これにより、例外はチェックされません。推奨される解決策は、キャッチオールハンドラーまたはthrows句を使用しますが、一般的なスーパークラスではなく、スローされる可能性のあるすべての例外の特定のスーパークラスを使用することです。別の推奨される解決策は、呼び出されるメソッドの抽象化レベルに適した例外型を定義して宣言し[ 44 ] 、例外チェーンを使用して下位レベルの例外をこれらの型にマッピングすることです。throwsExceptiontry{...}catch(Exceptione){...}try{...}catch(Throwablet){...}java.lang.RuntimeExceptionjava.lang.Throwable
Kotlinにはチェック例外がなく、KotlinからJavaのチェック例外をスローしても、クライアント側でJava側で例外を処理するように強制されるわけではありません。しかし、@Throwsアノテーションを使用することでこれを有効にでき、メタデータをJVMに出力できますthrows。例えば、次のKotlinコードを考えてみましょう。
import java.io.IOException@Throws ( IOException :: class ) fun readFile () { // ... throw IOException ( "ファイルの読み込みに失敗しました!" ) }これはJVMではおおよそ以下のように解釈されます。
import java.io.IOException ;public static final void readFile () throws IOException { // ... throw new IOException ( "ファイルの読み込みに失敗しました!" ); }チェック例外の起源は、CLU プログラミング言語の例外仕様の概念に遡ります。[ 45 ]関数は、その型にリストされている例外のみを発生させることができますが、呼び出された関数から漏れた例外は、failureコンパイル時エラーになるのではなく、自動的に唯一の実行時例外に変換されます。[ 46 ]後に、Modula-3 にも同様の機能がありました。[ 47 ]これらの機能には、チェック例外の概念の中心となるコンパイル時チェックは含まれていません。[ 45 ]
C++プログラミング言語の初期バージョンには、チェック例外に似たオプションのメカニズムである例外仕様が含まれていました。throwデフォルトでは、どの関数もあらゆる例外をスローできましたが、関数シグネチャに句(Javaの句に類似)を追加することで、関数がスローできる例外の種類を制限できました。たとえば、次のコードはC++03では有効でした。throws
#include <stdexcept>using std :: domain_error ; using std :: invalid_argument ;// これは Java のシグネチャに似ている可能性があります// void performSomeOperation(int a, int b) throws InvalidArgumentException, ArithmeticException; void performSomeOperation ( int a , int b ) throw ( invalid_argument , domain_error ) { // ... }C++のthrow句では、任意の数の任意の型を指定できます。これには、プリミティブ型や拡張されていないクラスも含まれますstd::exception(C++は任意の型のオブジェクトをスローすることをサポートするため)。throw句に型が指定されていない場合は、その関数は例外をスローしないことを示します。
例外指定はコンパイル時には強制されませんでした。違反すると、標準ライブラリ関数std::unexpected()[注 3 ]が呼び出されました。[ 48 ] 空の例外指定を指定すると、関数が例外をスローしないことを示します。例外処理が言語に追加されたときにこれがデフォルトにならなかったのは、既存のコードの変更が多すぎ、他の言語で書かれたコードとのやり取りが妨げられ、プログラマがローカルレベルでハンドラを多すぎるほど書くように誘惑されるからです。[ 48 ]ただし、空の例外指定を明示的に使用すると、関数内で例外処理が行われる可能性がある場合には不可能な、重要なコードとスタックレイアウトの最適化を C++ コンパイラが実行できるようになります。[ 24 ] 一部のアナリストは、C++ で例外指定を適切に使用することは難しいと考えていました。[ 49 ]この例外指定の使用はC++98およびC++03に含まれていましたが、2012 年の C++ 言語標準 ( C++11 )で非推奨となり、 [ 50 ] C++17で言語から削除されました。throws 句は 句に置き換えられました。例外をスローしない関数は、キーワードで示されるようになり、代わりに関数が例外をスローすることを指定します。句は言語から削除されましたが、シグネチャにのみ記述することは合法であり、 と同等です(句で例外が指定されていないということは、例外をスローできないことを意味します)。ただし、これはまだ非推奨とみなされています。句を使用するコードベースを移行するには、削除された をマクロとして再定義できます。これは、チェック例外の実際の実装ではなく、コードをコンパイルできるようにするための一時的な修正にすぎません。これを使用すると、同様に Java句を模倣できます。noexceptnoexceptnoexcept(false)throwthrow()noexceptthrowthrowthrowthrows
// throw() が空の場合、noexcept(true) (noexcept と同じ) に展開されます。// それ以外の場合は、noexcept(false) に展開されます。// 括弧のない throw ステートメントは展開されません。#define throw(...) noexcept(__VA_OPT__(!)true)import std ;std :: runtime_errorを使用します。class XException : public runtime_error {}; class YException : public runtime_error {};// throw(Es...) は noexcept(false) に展開されますvoid performSomeOperation ( int x , int y ) throw ( XException , YException ) { if ( x > y ) { // 括弧なしの throw はマクロとして展開されませんthrow XException ( "x > y" ); } else if ( y > x ) { throw YException ( "y > x" ); } std :: println ( "x = y = {}" , x ); }// throw() は noexcept(true) に展開されますvoid willNotThrow ( int x ) throw () { std :: println ( "x = {}" , x ); }また、ある関数がnoexcept別の関数が特定の条件を満たすことを条件として定義することもできますnoexcept。例えば、次のように記述します。
void mightThrow ();// 最初の noexcept は noexcept 句、2 番目はブール値に評価される noexcept 演算子です。void f () noexcept ( noexcept ( mightThrow ()));C++にはチェック例外はありませんが、ブロック内でスローされたオブジェクトをスタックに伝播させることができます。そのためには、(オブジェクトを指定せずに)catchと記述します。これにより、キャッチされたオブジェクトが再スローされます。こうすることで、オブジェクトをキャッチしたブロック内で処理を行い、その後、オブジェクトがさらに上位に伝播するかどうかを選択できます。throw;catch
OCamlプログラミング言語には、捕捉されない例外アナライザーが存在します。 [ 51 ]このツールは、発生した例外のセットを拡張型シグネチャとして報告します。ただし、チェック例外とは異なり、このツールは構文注釈を必要とせず、外部ツールです (つまり、例外をチェックせずにプログラムをコンパイルして実行できます)。
C++でも「ポケモン例外処理」を行うことができます。Javaと同様に、C++も例外ブロックをサポートしており、スローされたオブジェクトをすべてキャッチします。ただし、キャッチされたオブジェクトに名前が付けられないため、参照できないという欠点があります。これは、Javaのような言語では、拡張クラスのみがスローされる可能性があるのに対し、C++ではあらゆる型(プリミティブ型を含む)がスローされる可能性があるため、キャッチオールブロックでキャッチされたオブジェクトへの参照を安全に保管する方法がないためです。catch(Throwablet)catch(...)catch(...)java.lang.Throwable
import std ;std :: exceptionを使用します。// 例外のみをキャッチする場合: try { // ... } catch ( const exception & e ) { // 例外のみをキャッチする場合: std :: println ( "例外がキャッチされました: {}" , e . what ()); } catch (...) { // スローされたすべてのオブジェクトをキャッチする場合: std :: println ( "不明なエラーがキャッチされました" ); }Rust言語では、例外をまったく使用するのではなく、回復可能な例外を結果型として表現します。[ 52 ] [ 53 ]これはResult<T, E>(またはexpected<T, E>C++ では) として表現されます。結果型がチェック例外よりも優れている点は、結果型とチェック例外の両方がユーザーにエラーを即座に処理することを強制する一方で、チェック例外とは異なり、言語の型システム内で戻り値の型として直接表現できることです。チェック例外では、宣言された潜在的にスローされる例外は関数のシグネチャの一部ですが、戻り値の型には直接含まれません。
例外処理ルーチンの目的は、コードがエラー状態を適切に処理できることを保証することです。例外処理ルーチンが十分に堅牢であることを確認するには、ソフトウェア障害注入やミューテーションテスト(ファズテストとも呼ばれる)によって生成されるような、広範囲にわたる無効な入力や予期しない入力をコードに与える必要があります。例外処理ルーチンを作成するのが最も難しいソフトウェアの1つはプロトコルソフトウェアです。堅牢なプロトコル実装は、関連する仕様に準拠しない入力を受け入れる準備をしなければならないからです。
ソフトウェア開発ライフサイクル全体を通して有意義な回帰分析を実施するためには、例外処理テストを高度に自動化し、テストケースを科学的かつ再現可能な方法で生成する必要があります。このようなテストを実行できる市販のシステムはいくつか存在します。
Javaや.NETなどのランタイムエンジン環境では、ランタイムエンジンに接続するツールが存在し、関心のある例外が発生するたびに、例外がスローされた時点でメモリに存在していたデバッグ情報(コールスタックとヒープ値)を記録します。これらのツールは、自動例外処理ツールまたはエラーインターセプトツールと呼ばれ、例外の「根本原因」情報を提供します。
非同期例外は、 Ctrl-Cを押してプログラムを中断したり、シグナルを受信したり、別の実行スレッドから「停止」や「一時停止」などの中断メッセージを送信したりするなど、別のスレッドまたは外部プロセスによって発生するイベントです。[ 54 ] [ 55 ]同期例外は特定のthrowステートメントで発生しますが、非同期例外はいつでも発生する可能性があります。そのため、コンパイラは非同期例外がないことを証明できないため、非同期例外処理を最適化して削除することはできません。また、非同期例外はリソースリークを避けるためにクリーンアップ操作中にブロックする必要があるため、正しくプログラミングすることも困難です。
プログラミング言語は通常、非同期例外処理を回避または制限します。たとえば、C++ はシグナル ハンドラからの例外の発生を禁止しており、Java はjava.lang.ThreadDeathJava 20 で、あるスレッドが別のスレッドを停止できるようにするために使用されていた error の使用を非推奨にしました。[ 56 ]もう 1 つの機能は、プログラムの特定の操作中にのみ非同期例外を発生させる半非同期メカニズムです。たとえば、Java の は、java.lang.Thread::interrupt()スレッドが をスローする操作を呼び出したときにのみスレッドに影響しますjava.lang.InterruptedException。[ 57 ]同様の POSIX API には、安全に使用できない競合状態があります。[ 58 ]pthread_cancel
Common Lisp、R、[ 59 ] Dylan、Smalltalkには、前述の例外処理システムを包含する条件システム[ 60 ](Common Lisp条件システムを参照)があります。これらの言語または環境では、条件の発生( Kent Pitmanによれば「エラーの一般化」)は関数呼び出しを意味し、例外ハンドラの後半になって初めてスタックを巻き戻す決定が下される可能性があります。
条件は例外の一般化です。条件が発生すると、適切な条件ハンドラがスタック順に検索され、選択されて条件を処理します。エラーを表さない条件は、完全に処理されなくても安全です。その唯一の目的は、ユーザーにヒントや警告を伝えることかもしれません。[ 61 ]
これは、例外処理のいわゆる再開モデルに関連しており、一部の例外は継続可能であると言われています。つまり、ハンドラで修正処理を行った後、例外を通知した式に戻ることが許可されます。条件システムは次のように一般化されます。深刻でない条件(継続可能な例外とも呼ばれる)のハンドラ内では、通知式と条件ハンドラの間にある、あらかじめ定義された再開ポイント(再開とも呼ばれる)にジャンプすることができます。再開は、何らかの字句環境に対して閉じられた関数であり、プログラマが条件ハンドラを完全に終了したり、スタックを部分的に巻き戻したりする前に、この環境を修復することを可能にします。
例えば、PL/I のENDPAGE条件が挙げられます。ON ユニットは次のページのページトレーラー行とヘッダー行を書き込み、その後、中断されたコードの実行を再開します。
さらに、条件処理はメカニズムとポリシーの分離を実現します。再起動はエラーからの回復のための様々なメカニズムを提供しますが、特定の状況においてどのメカニズムが適切かを選択するわけではありません。それは条件ハンドラの役割であり、条件ハンドラは(より上位のコードに配置されているため)より広い視野を持つことができます。
例えば、単一のsyslogファイルエントリを解析するライブラリ関数があるとします。エントリの形式が不正な場合、この関数は何をすべきでしょうか? 正解は一つではありません。同じライブラリがさまざまな目的のプログラムで使用される可能性があるからです。対話型のログファイルブラウザーでは、解析せずにエントリを返してユーザーが確認できるようにするのが適切かもしれません。しかし、自動ログ要約プログラムでは、読み取り不可能なフィールドに null 値を指定し、不正なエントリが多すぎる場合はエラーで処理を中止するのが適切かもしれません。
つまり、この質問には、汎用ライブラリ関数には知られていないプログラムのより広範な目標に基づいてのみ回答できます。とはいえ、エラーメッセージで終了することが正しい回答となることは稀です。そのため、単にエラーで終了する代わりに、関数は、ログエントリをスキップしたり、読み取り不可能なフィールドにデフォルト値または null 値を指定したり、不足している値をユーザーに尋ねたり、スタックを巻き戻してエラーメッセージで処理を中止したりするなど、さまざまな続行方法を提供する再起動を確立することができます。提供される再起動は、エラーから回復するために利用できるメカニズムを構成します。条件ハンドラによる再起動の選択は、ポリシーを提供します。
ソフトウェアでは、特に例外の発生源が複数ある場合、例外処理が正しく行われないことがよくあります。500万行のJavaコードのデータフロー分析では、1300を超える例外処理の欠陥が見つかりました。 [ 13 ] WeimerとNeculaは、他の研究者による複数の先行研究(1999~2004年)と自身の研究結果を引用し、例外の重大な問題点は「プログラマが理解しにくい隠れた制御フローパスを作成する」ことだと述べています。[ 13 ] : 8:27 「try-catch-finallyは概念的には単純ですが、言語仕様[Gosling et al. 1996]の中で最も複雑な実行記述を持ち、公式の英語記述では4段階のネストされた「if」文を必要とします。つまり、プログラマが見落としがちな多数のコーナーケースが含まれています。」 [ 13 ] : 8:13–8:14
例外は、構造化されていない処理フローであるため、リソースリーク(ミューテックスでロックされたセクションや、一時的にファイルを開いたままにしているセクションからの脱出など)や状態の不整合のリスクを高めます。例外発生時のリソース管理にはさまざまな手法がありますが、最も一般的なのは、 dispose パターンと何らかのアンワインド保護(例えば、句など)を組み合わせたものですfinally。これにより、コードのセクションから制御が抜けたときにリソースが自動的に解放されます。
1980年、トニー・ホーアはAdaプログラミング言語について、「…機能や表記規則が多すぎるが、その多くは不要であり、例外処理のように危険なものもある。[…] 信頼性が極めて重要なアプリケーションでは、この言語を現状のまま使用してはならない。[…] プログラミング言語のエラーによって次に軌道を外れるのは、金星への無害な旅に出ている探査ロケットではなく、我々の都市の1つで爆発する核弾頭かもしれない」と述べている。[ 62 ]
Go の開発者は、try-catch-finally イディオムが制御フローを難読化すると考えており、[ 63 ]例外のようなpanic/recoverメカニズムを導入しました。[ 64 ] は、関数内のコード ブロック内からのみ呼び出すことができるため、ハンドラはクリーンアップと関数の戻り値の変更のみを行うことができ、関数内の任意の場所に制御を戻すことはできません。[ 65 ]ブロック自体は、句と同様に機能します。recover()catchdeferdeferfinally
Rust言語には例外処理がありません。代わりに、実行時エラーの処理には(結果型)を使用し、重大なエラーの場合はマクロを使用します。Result<T,E>panic!()
throw制限される可能性もあります。std::unexpected<T, E>-catch-finallyイディオムのように、例外を制御構造に結合させると、コードが複雑になると考えています。また、ファイルを開けないなどの通常のエラーを例外として扱うことをプログラマーに促す傾向があります。
具体的な制限は、recoverはdeferコードブロック内でのみ呼び出すことができ、任意のポイントに制御を戻すことはできず、クリーンアップと関数の戻り値の微調整のみを行うことができるという点です。