サイトアイコン Embarcadero RAD Studio, Delphi, & C++Builder Blogs

C++Builder 12.2でC++のEHとSEHの例外処理を分割する

例外処理には、複数の種類があり、それらを混在させることは正しくありません。例えば、C++ 例外処理と SEH 例外処理を混在するべきではありません。なぜならば、コードは安全ではなく、そして動作しない可能性があるためです。 C++Builder 12.2では、C++のEHとSEHの例外処理が混在したコードを分割するために便利なツールが提供されています。

C++例外処理と SEH例外処理の混在について

C++の例外は、C++オブジェクト (int を含む任意の型) を送出する場合に発生します。例えば、C++の例外処理ではtry-catchです。

[crayon-675acec0e902e509740195/]

構造化例外処理(SEH)は純粋なC++の概念ではなく、C++でサポートするために後から追加されたキーワードがあります。それはRaiseException() API(すなわちthrowではない)を介して発生する例外、またはハードウェアやカーネルの例外の類です。SEH処理メカニズムとして__try/__filter()ではなく__try/__finallyを使用している方が多いのではないでしょうか。

[crayon-675acec0e9037046697996/]

C++Builderではどちらも全く問題なく使用できます。実際、C++Builder 12.2ではSEHが大幅に改善され、以前のバージョンに比べて適切に機能しています。

問題は、以下のように 2 つを混在させた場合です。

[crayon-675acec0e9039262892028/]

上記のコード例では、EH処理メカニズムがC++ EH処理メカニズムの内側に直接定義されています。逆の方法で混在させることもできます。ポイントは、同じコード内に2つのEHメカニズムが定義されていることです。

何が悪いのでしょうか?それはCodeGenです。Codegenは、2つの制御フローメカニズム、オブジェクトのアンワインド(巻き戻し)するための 2つのトリガーなどを使用して、同じ「フレーム」(同じメソッド、コールスタックのエントリと考えてください)で2つの異なる種類の例外処理をサポートすることはほぼ不可能です。動作の異常さを理解する一例として、次のようなルーチンを想像してみてください。「1つのメソッドにさまざまな戻り値があり、たくさんの goto文もあるとします。ループ内で goto したり、他のロジック構造内で goto したりできます。さらにgotoと混在させるために他の制御フロー(setjmp/longjmpなど)を追加したため、実行はあちこち移動し、移動には異なるシステムを使用していました。その後、デストラクタなどのすべてのスコープ終了動作の呼び出しを含め、コードが正しいことを確認する必要がある」という、このような複数の例外処理と制御フローのメカニズムが作り出すCodegenの例です。Visual C++ではこのような動作をサポートしておらず、エンバカデロでも同様にサポートしていません。

しかし、以前はこのようなコードを記述することができ、コンパイラもそれを許容していましたが、現在ではエラーが発生します。以前は動作していたコードが動作しなくなったように見えるかもしれませんが、実際はそうではありません。その古いコードのコード生成がめちゃくちゃ間違っており、メモリリークなどの例外が発生していた可能性が高いのです。デストラクタが正しい順序で呼び出されていない、あるいはまったく呼び出されていないなどの問題が発生していた可能性もあります。つまり、間違っているが、そのことに気づかなかったというケースです。

注:C++Builder 12.2の大きなテーマは、新しいコンパイラが以前は危険だったコードをより安全に実行できるようにすることです。エンバカデロでもこの目的のために新しいコンパイラを採用しています。コンパイラの安全性については、こちらのブログを参照ください。

修正する方法は?

同じフレーム内で 2 つの異なる種類の例外処理を混在させるコードは間違っており、トラブルを招く原因に繋がるため、絶対にすべきではありません。そのためコード内に2 つの異なる種類の例外処理が混在している場合は、例外処理を行っているコードを分割する必要があります。

2種類の例外処理は、同じアプリケーション内に共存でき、同じコードパスやコールスタック内に共存できますが、同じメソッド内には共存できません。つまり、分割とは、try/catchまたは__try/finallyブロック内のコードを切り出して、新しいメソッドに記述することです。

以下のような間違ったコードを例に見てみましょう。

[crayon-675acec0e903b280191176/]

ここでクイズですが、__tryブロックで例外が発生した場合(例えば、Widthプロパティの設定でアクセス違反が発生した場合)、返される値は、m_pBitmap.get() でしょうか? それとも nullptr でしょうか?

これを整理するために、__try/__finallyのペア全体を新しいメソッドに移動させたいと思います。

C++Builderでは、この作業が簡単に行えるツール機能があります。 下図のように該当するコードを範囲選択し、マウスを右クリックして「リファクタリング」を選択し、「メソッドの抽出」を選択してください。C++Builder 12.2の新しいリファクタリングは、こちらをご参照ください。

「メソッドの抽出」を行い、新しいメソッドに移動させた結果は、以下のようになります。

注: パラメータの処理に注意してください。パラメータを正しく使用して、ローカル変数、最初のメソッドからのパラメータなどを渡し、必要に応じて const または参照にします。ここでは、変更されない単純な値型であったため、const-by-value でした。

このリファクタリングは、まさにこのようなユースケースのために C++Builder 12.2 で「メソッドの抽出」が追加されました。メソッドの抽出は、コードブロックを取り出して新しいメソッドに追加します。これはスマートな機能で、変数を処理し、抽出されたメソッドにパラメータを作成し、必要に応じて参照を作成します。

したがって、もしコンパイラエラーが表示された場合は、どうすればよいか察しがつくと思いますが、コードを選択し、リファクタリングして、メソッドを抽出してコーディング作業を続行してください。このように一度、コードのメンテナンスを行うだけで、ご自身が見落としていたバグが解決されるだけでなく、例外処理の信頼性もさらに向上いたします。

ウェビナーについて

本ブログの内容は、エンバカデロのC++のブロダクトマネージャである「David Millington」が、ウェビナー内で「Tip #5, Take Advantage of Security and Correctness」で解説しています。このウェビナーはC++Builder / RAD Studio 12.2の新しいWin64 モダンツールチェインを活用するためのヒントや非常に有益な情報を紹介しています。もしウェビナー(英語)をご覧になりたい場合は、以下をご参照ください。

「Tip #5: Split out C++ EH and SEH exception handling」の節は、下記の動画の「1:14:50」です。

モバイルバージョンを終了