例外処理、第 17 部

Robert Schmidt
Microsoft Corporation

2000 年 3 月 16 日

私はずっと前に、このコラム シリーズの第 2部 で構造化例外処理(別名 SEH)を紹介しました。そこで説明したように、SEH は Windows と Windows 用コンパイラに特有です。SEH は ISO C++ 標準規格で定義されていません。また、SEH を使用しているプログラムはコンパイラ間で移植性がありません。私は標準規格への準拠と移植性を旨としているので、Windows 特有の SEH を ISO 標準規格の C++ 例外処理(ここでは C++EH と略します)にマップすることに興味を持ちました。

しかし、私は SEH の権威ではありません。私が SEH について知っていることは、ほとんどこれらのコラムのために調査した中で得た知識です。SEH を C++EH に対応させることを考えたとき、正直言って私は、解決策が難しくて分かりにくいものになると予想しました。それで私は、先月 2 週間の休みを取ったのです。調査とテストに、もっと時間が必要だと予想したからです。

嬉しいことに、私は大きく間違っていました。私のやりたかったことのうち、かなりの部分を Visual C++ のランタイム ライブラリが直接サポートしているということを、私は知らなかったのです。したがって、新しい方法を考え出す代わりに、Visual C++ がどのようなサポート機能を既に提供していて、そのサポート機能を自分の仕事に適用する方法を示します。これらの目的に向けて、私は 1 例を挙げて、これの異なるバージョンを 4 つ、詳しく調べました。

例 1:トランスレータ関数の定義

SEH を C++EH に結び付ける「グルー(糊)」は、2 つの部分に分かれます。

  • 構造化例外をキャッチして、それらを C++ 例外にマップする、ユーザー定義のトランスレータ関数

  • トランスレータをインストールする、Visual C++ のランタイム ライブラリ関数

ユーザー定義のトランスレータは、以下のようでなければなりません。

  void my_translator(unsigned code, EXCEPTION_POINTERS *info);

トランスレータは、指定された例外の codeinfo によって定義された構造化例外を受け取ります。逆にトランスレータは、C++ 例外をスローすることができます。これによって、受け取った構造化例外を、外部に渡す C++ 例外にマップできます。C++ 例外は、オリジナルの構造化例外のポイントから伝搬しているように見えるでしょう。

このメカニズムは、std::set_terminatestd::set_unexpected のメカニズムによく似ています。トランスレータをインストールするために、Visual C++ のライブラリ関数 _set_se_translator を呼び出します。この関数はヘッダの eh.h で、以下のように宣言されます。

  typedef void (*_se_translator_function)(unsigned, EXCEPTION_POINTERS *);

_se_translator_function _set_se_translator(_se_translator_function);

関数は、新しいトランスレータへのポインタを受け取り、先にインストールされているトランスレータを返します。新しいトランスレータをインストールすると、以前のものは失われれます。同時に 2 つのトランスレータはアクティブになりません(マルチスレッドのプログラムでは、各スレッドに個別のトランスレータがあります)。

トランスレータが先に設定されていなければ、_set_se_translator への最初の呼び出しは、NULL を返す(または、返さない)かもしれません。つまり、_set_se_translator によって返されるポインタを介した、むやみな呼び出しはできないことになります。面白い皮肉ですが、_set_se_translator によって NULL が返され、その NULL ポインタを介して呼び出しを行おうとすると、構造化例外が生成されます。そして、インストールしたばかりのトランスレータに到着することになります。

簡単な実施例を以下に示します。

  #include <iostream>
using namespace std;

int main()
   {
   try
      {
      *(int *) 0 = 0; // generate Structured Exception
      }
   catch (unsigned exception)
      {
      cout <<"caught C++ exception " <<hex <<exception <<endl;
      }
   return 0;
   }

実行すると、このコンソール プログラムは、次のような Windows のメッセージ ボックスを出すでしょう。

構造化例外が発生し、それがキャッチされずに、プログラム境界を越えたことから、メッセージ ボックスが表示されています。

では、例外トランスレータを加えてみましょう。そして、Visual C++ のランタイム ライブラリを設定して、そのトランスレータを使用するようにします。

  #include <iostream>
using namespace std;

#include "windows.h"

static void my_translator(unsigned code, EXCEPTION_POINTERS *)
   {
   throw code;
   }

int main()
   {
   _set_se_translator(my_translator);
   try
      {
      *(int *) 0 = 0; // generate Structured Exception
      }
   catch (unsigned exception)
      {
      cout <<"caught C++ exception " <<hex <<exception <<endl;
      }
   return 0;
   }

もう一度プログラムを実行してください。今度は、以下のように表示されるはずです。

  caught C++ exception c0000005

my_translator は構造化例外を横取りします。このときトランスレータは、構造化例外を unsigned 型の C++ 例外に変換し、構造化例外のコード(この場合は C0000005h。不正アクセスのエラーを表します)を含めます。C++ 例外がオリジナルの構造化例外のポイント(try ブロックの中)から生じるように見えるので、C++ 例外は try ブロックのハンドラにキャッチされます。

例 2:トランスレータ オブジェクトの定義

上記の例は比較的単純で、各構造化例外を 1 つの unsigned 値に変換しています。現実には、おそらくもっと複雑で、もっと上手く抽象化された例外オブジェクトが必要になるはずです。

  #include <iostream>
using namespace std;

//#include "windows.h"

#include "structured_exception.h"

/*static void my_translator(unsigned code, EXCEPTION_POINTERS *)
   {
   throw code;
   }*/

int main()
   {
//_set_se_translator(my_translator);
   structured_exception::install();
   try
      {
      *(int *) 0 = 0; // generate Structured Exception
      }
   catch (structured_exception const &exception)
      {
      cout <<"caught C++ exception " <<hex <<exception.what()
            <<" thrown from " <<exception.where() <<endl;
      }
   return 0;
   }

この例は、ユーザー定義の structured_exception 型の C++ 例外をスローします。この例を、もっと現実的で読みやすいものにするために、structured_exception インターフェイスをヘッダ ファイル structured_exception.h に分割しました。

  #if !defined INC_structured_exception_
    #define  INC_structured_exception_

#include "windows.h"

class structured_exception
   {
public:
   structured_exception(EXCEPTION_POINTERS const &) throw();
   static void install() throw();
   unsigned what() const throw();
   void const *where() const throw();
private:
   void const *address_;
   unsigned code_;
   };

#endif // !defined INC_structured_exception_

そして、structured_exception の実装をソース ファイルへ入れます。

  #include "structured_exception.h"

#include "eh.h"

//
//  ::
//
static void my_translator(unsigned, EXCEPTION_POINTERS *info)
   {
   throw structured_exception(*info);
   }

//
//  structured_exception::
//
structured_exception::structured_exception
      (EXCEPTION_POINTERS const &info) throw()
   {
   EXCEPTION_RECORD const &exception = *(info.ExceptionRecord);
   address_ = exception.ExceptionAddress;
   code_ = exception.ExceptionCode;
   }

void structured_exception::install() throw()
   {
   _set_se_translator(my_translator);
   }

unsigned structured_exception::what() const throw()
   {
   return code_;
   }

void const *structured_exception::where() const throw()
   {
   return address_;
   }

関数の意味は、以下のようになります。

  • my_translator は、例外トランスレータ関数です。これを私は、メイン ソース ファイルにあるオリジナルの場所から移動しました。その結果、メイン ソース ファイルは、今後 windows.h をインクルードする必要がなくなりました。

  • install は、ランタイム ライブラリのグローバルなトランスレータを my_translator に設定します。

  • structured_exception コンストラクタは、構造化例外の情報を受け取って解析します。

  • what は構造化例外のコードを返します。

  • where は、構造化例外のもとのアドレスを返します。C++ 標準規格では、コードのアドレスを void ポインタに変換することを許していませんが、where の戻り型は void const * です。この点に注意してください。ここでは、単に Microsoft の使用法を真似ているだけです。というのも、Visual C++ のライブラリが、構造化例外の EXCEPTION_RECORDvoid * メンバにアドレスを格納するからです。

これら 3 つのファイルを、コンパイルしてリンクします。これを実行すると以下のような結果が得られます。

  caught C++ exception c0000005 thrown from 0040181D

(最終的なコード アドレス値は、あなたのシステムでは異なるかもしれません。)

例 3:標準ライブラリを真似てみる

my_translator では、構造化例外はすべて、同じ structured_exception 型にマップします。例外がすべて次の単独のハンドラに対応するため、例外がキャッチしやすくなります。

  catch (structured_exception const &exception)

しかし、例外を一度キャッチすると、例外の本来の種類に関する情報が得られなくなります。そうなると、頼みの綱は、例外の what メンバへのランタイム クエリだけになります。

  catch (structured_exception const &exception)
   {
   switch (exception.what())
      {
      case EXCEPTION_ACCESS_VIOLATION:
         // ...
      case EXCEPTION_INT_DIVIDE_BY_ZERO:
         // ...
      case EXCEPTION_STACK_OVERFLOW:
         // ...
      // ...
   }

このようなクエリをする場合、クライアントは windows.h がインクルードしていて、オリジナルの構造化例外のコードの意味を知っている必要があります。こうした情報を知っていることは、structured_exception の抽象化に違反します。さらに、手の込んだ switch ステートメントはしばしば、ポリモーフィックな関係を示します。クライアント コードの観点からすれば、一般に、こういった関係は、継承やテンプレートで実装すべきです。

この点について、C++ 標準ライブラリがガイダンスを提供しています。このシリーズの第3部で述べたように、ヘッダ <stdexcept>std::exception をルートとする例外クラスの階層を定義します。そのルート クラスは、virtual メンバ what を宣言します。これは実装によって定義される NTBS(「ヌルで終わるバイト文字列」の標準規格用語)を返します。派生クラスは、それぞれ what が返す値を指定します。標準規格は、これらの値を特に規定していませんが、標準化委員会はおそらく、文字列に各例外の種類や意味を説明させようとしていたのだと思います。

このスキーマを standard_exception のインターフェイスに適用してみます。

  #if !defined INC_structured_exception_
    #define  INC_structured_exception_

#include "eh.h"
#include "windows.h"

class structured_exception
    {
public:
   structured_exception(EXCEPTION_POINTERS const &) throw();
   static void install() throw();
   virtual char const *what() const throw();
   void const *where() const throw();
private:
   void const *address_;
   //unsigned code_;
   };

class access_violation : public structured_exception
   {
public:
   access_violation(EXCEPTION_POINTERS const &) throw();
   virtual char const *what() const throw();
   };

class divide_by_zero : public structured_exception
   {
public:
   divide_by_zero(EXCEPTION_POINTERS const &) throw();
   virtual char const *what() const throw();
   };

#endif // !defined INC_structured_exception_

そして、実装します。

  #include <exception>
using namespace std;

#include "structured_exception.h"

#include "windows.h"

//
//  ::
//
static void my_translator(unsigned code, EXCEPTION_POINTERS *info)
   {
   switch (code)
      {
      case EXCEPTION_ACCESS_VIOLATION:
         throw access_violation(*info);
         break;
      case EXCEPTION_INT_DIVIDE_BY_ZERO:
      case EXCEPTION_FLT_DIVIDE_BY_ZERO:
         throw divide_by_zero(*info);
         break;
      default:
         throw structured_exception(*info);
         break;
      }
   }

//
//  structured_exception::
//
structured_exception::structured_exception
      (EXCEPTION_POINTERS const &info) throw()
   {
   EXCEPTION_RECORD const &exception = *(info.ExceptionRecord);
   address_ = exception.ExceptionAddress;
   //code_ = exception.ExceptionCode;
   }

void structured_exception::install() throw()
   {
   _set_se_translator(my_translator);
   }

char const *structured_exception::what() const throw()
   {
   return "unspecified Structured Exception";
   }

void const *structured_exception::where() const throw()
   {
   return address_;
   }

//
//  access_violation::
//
access_violation::access_violation
      (EXCEPTION_POINTERS const &info) throw()
      : structured_exception(info)
   {
   }

char const *access_violation::what() const throw()
   {
   return "access violation";
   }

//
//  divide_by_zero::
//
divide_by_zero::divide_by_zero
      (EXCEPTION_POINTERS const &info) throw()
      : structured_exception(info)
   {
   }

char const *divide_by_zero::what() const throw()
   {
   return "divide by zero";
   }

注意点:

  • クライアントの例外ハンドラで要求された switch ステートメントは、ここでは my_translator でカプセル化されています。すべての構造化例外を 1 つの値(例 1 のように)や、1 つのオブジェクト型(例 2)にマップする代わりに、my_translator は実行時のコンテキストに応じて、複数のオブジェクト型の 1 つにマップします。

  • structured_exception は基本クラスになりました。これを抽象基本クラスにすることも考えましたが、標準ライブラリに従うことに決めました(std::exception を具象基本クラスです)。

  • デストラクタは定義しません。これらの単純なクラスについては、コンパイラによって提供される暗黙のデストラクタで十分だからです。デストラクタを定義したとしたら、virtual である必要があります。

  • what のオーバーライドは、オリジナルの構造化例外コードの代わりに、分かりやすいテキストを返します。

  • もう例外コードをテストしたり、表示したりしないので、データ メンバ code_ を削除しました。これで structured_exception オブジェクトのサイズは、もっと小さくなります(あんまり興奮しないでください。このスペースの節約は、各オブジェクトの新しい v-pointer によって相殺されます)。

  • テンプレート ベースのパターンを代わりに採用すれば、ここで示した継承パターン放棄してもかまいません。これは、みなさんの演習として残しておきます。

この新しいスキーマをテストするために、メインのソース ファイルを書き換えます。

  #include <iostream>
using namespace std;

#include "structured_exception.h"

int main()
   {
   structured_exception::install();
   //
   //  discriminate exception by dynamic type
   //
   try
      {
      *(int *) 0 = 0; // generate Structured Exception
      }
   catch (structured_exception const &exception)
      {
      cout <<"caught " <<exception.what() <<endl;
      }
   //
   //  discriminate exception by static type
   //
   try
      {
      static volatile int i = 0;
      i = 1 / i; // generate Structured Exception
      }
   catch (access_violation const &)
      {
      cout <<"caught access violation" <<endl;
      }
   catch (divide_by_zero const &)
      {
      cout <<"caught divide by zero" <<endl;
      }
   catch (structured_exception const &)
      {
      cout <<"caught unspecified Structured Exception" <<endl;
      }
   return 0;
   }

そして、再実行します。結果は以下ようになります。

  caught access violation
caught divide by zero

例 4:標準ライブラリに適応させる

standard_exception 階層にあるクラスは、すべて次の public メンバを提供します。

  virtual char const *what() const;

これを使って例外の動的な型を識別します。この関数の名前、シグナチャ、意味は偶然に選んだものではありません。標準ライブラリの std::exception 階層のクラスはどれも、この同じ public メンバを、同じ目的のために公開します。結果的に、両方の階層でポリモーフィックな関数は、what だけになります。

この説明がどこに行き着こうとしているのか既に分かっている人もいるでしょうね。

  #include <exception>

class structured_exception : public std::exception
   {
public:
   structured_exception(EXCEPTION_POINTERS const &info) throw();
   static void install() throw();
   virtual char const *what() const throw();
   void const *where() const throw();
private:
   void const *address_;
   };

今度は structured_exceptionstd::exception なので、1 つのハンドラで両方の例外ファミリをキャッチすることができます。

  catch (std::exception const &exception)

そして、同じポリモーフィック関数を使って、例外の種類にアクセスすることができます。

  catch (std::exception const &exception)
   {
   cout <<"caught " <<exception.what();
   }

このようなスキーマでは、構造化例外は C++ 標準規格にとって「ネイティブ」なものとして扱えます。同時に、structured_exception を識別して、これらに特有のメンバにアクセスすることもできます。

  catch (structured_exception const &exception)
   {
   cout <<"caught Structured Exception from " <<exception.where();
   }

しかし、where など std::exception 階層には現れない基本メンバを諦める気があるなら、structured_exception そのものを排除して、std::exception 階層内のクラスから access_violation その他を直接派生させることができます。たとえば、ゼロによる除算の例外は、プログラマが制御可能な範囲のエラーを表します。しかし、これは同時にロジック上のエラーでもあります。したがって、divide_by_zero を直接 std::logic_error から、あるいは std::out_of_range からまで、派生させたい場合も考えられます。

標準ライブラリの例外階層をもっと理解するために、そして、その階層にカスタムの例外クラスを統合する最良の方法を知るために、C++ 標準規格の 19.1 節(「例外クラス」)を研究するよう勧めます。

勝負の終わり

以上で、私の例外処理武勇伝を終わります。正直言って、このシリーズがこれほど長くなるとは思いもしませんでした。しかし、このトピックに熱中すればするほど、取り上げたい事柄が出てきたのです。長丁場でしたが、その分だけよいものがお届けできたと思います。

このシリーズから何か1つだけを身に付けるとしたら、私が勧めるのは次の教訓です:例外は、どのような書かれ方をされようとも、倫理的な設計の本質的で避けられない一部なのです。例外は、多くの人が考えているように自由に決めたり行き当たりばったりで決めたりする「生き方の選択」ではありません。インターフェイス仕様は、例外のセマンティックスを明確に定義しなければなりません。実装は、それらのセマンティックスを正確かつ厳密にサポートしなければなりません。さらに、使用するインターフェイスの例外セマンティックを認識し、それに従わなければなりません。

私は、このシリーズを始める前までは、これらの真実を完全には信じていませんでした。しかし、書いているうちに信じるようになったのです。みなさんも同様であることを願いします。

Robert Schmidt は MSDN のテクニカル ライターです。このほかに彼が寄稿している雑誌に、C/C++ Users Journal があります。そこでは、彼は編集補助およびコラムニストとして活躍しています。これまでのキャリアで、ラジオ DJ、野生生物飼育係、天文学者、ビリヤード場管理者、探偵、新聞配達夫、そして大学講師を経験しています。

Deep C++用語集