More C++ Idioms/多態的例外(Polymorphic Exception)

多態的例外(Polymorphic Exception)
編集

意図 編集

  • 例外オブジェクトを多態的に作成する。
  • 送出されるかも知れない例外の具体的な詳細からモジュールを分離する。

別名 編集

動機 編集

よく知られたオブジェクト指向ソフトウェア設計のガイドラインである依存関係逆転の原則(Dependency Inversion Principle (DIP))によれば、より高位のモジュールはより低位のモジュールに直接依存するべきではない。代わりに、双方とも(しっかりと定義されたインタフェースの形で取り込まれた)共通の抽象に依存するべきである。例えば、ある Person 型のオブジェクト(John とする) は HondaCivic 型のオブジェクトを作成したり使うべきではない。代わりに、John は単純に Car インタフェース、HondaCivic の抽象基本クラスに委ねるべきである。これにより、John は将来簡単に Person クラスを変更することなく Corvette にアップグレードする事が出来る。John は依存性注入(dependency injection)技法を使って(任意の)車の具体的なインスタンスによって設定する事が出来る。DIP を用いる事で、簡単にユニットテストでき自由度が高く拡張可能なモジュールとなる。依存性注入によって実際のオブジェクトを簡単にモックオブジェクトに置き換えられる為、ユニットテストは DIP によって単純化される。

しかし、DIP が侵害されているタイミングもある。1 つは Singleton パターンの使用時であり、もう一つは例外の送出時である。Singleton パターンは静的な instance() 関数にアクセスする際、具体的なクラス名の使用を強制する為 DIP に違反する。Singleton は関数やコンストラクタを呼び出す際パラメータとして渡されるべきである。似たような状況が C++ で例外を取り扱う際に発生する。C++ の throw 節は例外を送出する為に具体的な型名(クラス)を必要とする。例えば、

throw MyConcreteException("Big Bang!");

このように例外を送出するモジュールは直ちに DIP の違反となる。もちろん、実際の例外オブジェクトを簡単にモックの例外オブジェクトに置き換えられない為、そのようなモジュールではユニットテストはより困難になる。下記のような解法は不幸にも失敗する。なぜなら C++ の throw は静的な型付けを使用し多態性について何も考慮しないからである。

struct ExceptionBase { };
struct ExceptionDerived : ExceptionBase { };
 
void foo(ExceptionBase& e)
{
   throw e; // 例外送出時の e の静的な型が使用される。
}
int main (void)
{
  ExceptionDerived e;
  try {
    foo(e);
  }
  catch (ExceptionDerived& e) {
    // foo の中で送出された例外はこの catch には合致しない。
  }
  catch (...) {
    // foo の中で送出された例外はここで捕捉される。
  }
}

多態的例外(Polymorphic Exception)イディオムはこの問題に対処する。

解法とサンプルコード 編集

多態的例外(Polymorphic Exception)イディオムは単純に、仮想関数 raise() を使うことで派生クラスに例外送出を委譲する。

struct ExceptionBase 
{ 
  virtual void raise() { throw *this; }
  virtual ~ExceptionBase() {} 
};
struct ExceptionDerived : ExceptionBase 
{ 
  virtual void raise() { throw *this; }
};
 
void foo(ExceptionBase& e)
{
   e.raise(); // 例外送出時の e の動的な型が使用される。
}
int main (void)
{
  ExceptionDerived e;
  try {
    foo(e);
  }
  catch (ExceptionDerived& e) {
    // foo の中で送出された例外は今やこの catch に合致する。
  }
  catch (...) {
    // もはやここには来ない!
  }
}

仮想関数中に throw 文が移動している。関数 foo 中で呼び出される raise 関数は多態的であり、ExceptionBaseExceptionDerived の実装がパラメータとして渡されたものによって選択される(依存性注入)。this の型はコンパイル時に明らかに分かる為、多態的な例外が送出される事になる。このイディオムの構造は仮想コンストラクタ(Virtual Constructor)イディオムに非常によく似ている。

多態的例外の伝播

一つの例外が、プログラムやライブラリの異なる階層で異なったやり方で複数の catch 文で処理されることは非常に良くある。そのような場合、より外側の catch ブロックが(存在するならば)必要な処理を行えるように先行する catch ブロックは例外を再送する必要がある。多態的な例外が関係する場合、内側の catch ブロックは外側の catch ブロックに渡す前に例外オブジェクトを変更するかも知れない。そのような場合、確実にオリジナルの例外オブジェクトが伝播されるよう注意を払わねばならない。一見した所無害に見えるが失敗している以下のプログラムを考えてみよう。

try {
    foo(e);// 前述のように ExceptionDerived クラスのインスタンスが送出される。
  }
  catch (ExceptionBase& e)// 基本クラスException は多態的に捕捉される事に注意。
  {
    // 例外を処理する。オリジナルの例外オブジェクトを変更するかも知れない。
    throw e; // 警告!オブジェクトのスライシングが発生している。
  }

throw e はオリジナルの例外オブジェクトを送出していない。代わりに、オリジナルのオブジェクトの一部分(ExceptionBase の部分)だけ切り出されたコピー(sliced copy)を送出する。なぜなら目の前にある例外の静的な型を考慮するからである。無言のうちに、派生された例外オブジェクトは失われるだけでなく基本型の例外オブジェクトに変換されてしまう。外側の catch ブロックでは、この catch が持っていたものと同じ情報にアクセスすることができない。この問題に対処する方法が二つある。

  • 単純に(いかなる式も続かない形で) throw を使う。これはオリジナルの例外オブジェクトを再送する。
  • 再度、多態的な例外イディオムを使用する。raise() 仮想関数が throw *this を使うためオリジナルの例外オブジェクトのコピーが送出される。


try {
    foo(e);// 前述のように ExceptionDerived クラスのインスタンスが送出される。
  }
  catch (ExceptionBase& e)// 基本クラスException は多態的に捕捉される事に注意。
  {
    // 例外を処理する。オリジナルの例外オブジェクトを変更するかも知れない。
    // 以下の二つの内いずれか一方のみを使用する。
    throw;     // 選択肢1:オリジナルの派生型例外が送出される。
    e.raise(); // 選択肢2:オリジナルの派生型例外オブジェクトのコピーが送出される。
}

既知の利用 編集

関連するイディオム 編集

仮想コンストラクタ(Virtual Constructor)

References 編集