280 実行されて欲しいわけです。 第 11 章マルチメソッド 全ての描画オプジェクトは Shape へのポインタとして操作されるため , 適切なアルゴリズムを選択するため に必要な型情報は , 手元に存在しません。つまり , あなたは Shape へのポインタということしか判っていない わけです。そして , 2 つのオプジェクトが絡んでいるため , 単純な仮想関数でこの問題を解決することはできな いのです。こういった場合が , ダブル・ディスパッチの出番というわけです。 11.3 型の判定をニ重化する : カまかせに行う方法 ダブル・ディスパッチを実装する最も直截的なアプローチは , 型の判定を二重化することです。最初の引数に 対して , 左手側として可能な値を次々にダイナミック・キャストしてみるのです。そしてその結果毎に , 同じよ うなダイナミック・キャストを 2 番目の引数に対しても行うわけです。双方のオプジェクトの型が判明すれば , どの関数を呼び出すかが判ります。このコードは以下のようになります : / / 様々な交差アルゴリズム void void void void void void void DoHatchAreaI (Rectang1e&, RectangIe&) ; DoHatchArea2(Rectang1e& , EIIipse&) ; DoHatchArea3(Rectang1e& , PoIyD ; DoHatchArea4(EIIipse&, PoIy&) ; DoHatchArea5 (EIIipse&, EIIipse&) ; DoHatchArea6(P01y&, PoIy&) ; Doub1dDispatch (Shape& 1hs , Shape& rhs) if (Rectang1e* pl = dynamic—cast く Rectang1e*> (&lhs) ) if (Rectang1e* p2 = dynamic—cast く Rectang1e*>(&rhs) ) DoHatchArea1 (*pl , *p2) ; else if (E11ipse* p2 = dynamic—cast く E11ipse*>(&rhs)) DoHatchArea2 (*pl , *P2) ; else if (P01y* p2 = dynamic—cast く P01y*> (&rhs) ) DoHatchArea3 (*pl , *p2) ; else E てて。 r ( " 未定義の交差 " ) ; else if (E11ipse* pl dynamic-cast く E11ipse*> (&lhs) ) if (Rectang1e* p2 = dynamic-cast く Rectang1e*> (&rhs) ) DoHatchArea2 (*p2 , *pl) ; else if (E11ipse* p2 = dynamic—cast く E11ipse*>(&rhs)) DoHatchArea5(*p1 , *p2) ; else if (poly* p2 = dynamic—cast く poly*> (&rhs) ) DoHatchArea4(*pI , *p2) ; else Err 。 r ( " 未定義の交差 " ) ; else if (P01y* pl dynamic-cast く P01y*> ( & 1 s ) )
11.5 カまかせのディスパッチャにおける対称性 / / 引数の順序を逆にすることによって , / / Fire(Rectang1e&, E11ipse&) に転送する。 Fire(rhs, lhs) ; 289 こういった転送関数の保守は , 難しいものとなります。理想的には , 追加の bool テンプレート・パラメータを StaticDispatcher に引き渡して , 対称性が選択できるようにするべきでしよう。これは考えてみる価値があ ります。 これには , 特定のケースでコールバックが起動された際 , 引数の順序を逆にするような StaticDispatcher が必要となります。では , 特定のケースとはどういったケースでしようか ? 前の例を分析してみましよう。テ ンプレート引数リストをデフォルト値で展開すると , 以下のような実体化が得られます : typedef StaticDispatcher く HatcingExecutor , Shape , TYPELIST_3 (Rectang1e , Shape , TYPELIST_3 (Rectang1e , void HatchingDispatcher ; 対称型ディスパッチャにおいて , E11ipse, E11ipse, P01y) , P01y) , / / TypesLhs / / TypesRhs パラメータのペアを選択するアルゴリズムは以下のようになります。最初の タイプリスト (TypesLhs) 中の先頭の型を , 2 番目のタイプリスト (TypesRhs) 中の各型と組み合わせます。 これによって 3 種類の組み合わせ , RectangIe-Rectang1e, RectangIe-E11ipse, RectangIe-P01y が作り出 されます。次に , TypesLhs 中の 2 番目の型 (EIIipse) を TypesRhs 中の型と組み合わせます。しかし , 最初 の組み合わせ (Rectang1e-E11ipse) は , 最初のステップで既に作り出されているため , 今回は TypesRhs 中の 2 番目の要素から開始します。このステップによって , E11ipse-E11ipse, E11ipse-P01y が作り出されます。 次のステップも同じように適用すると , TypesLhs 中の poly は , Types s 中の 3 番目から始まる型 ( つまり 1 つだけ ) と組み合わされます。これによって , poly ー poly という組み合わせが作り出され , アルゴリズムはそこ で停止します。 このアルゴリズムに従えば , 作り出された組み合わせの関数のみを実装することになります : class HatchingExecutor public : void void void void void void Fire (Rectang1e&, Rectang1e&) ; Fire (Rectang1e& , EIIipse&) ; Fire (RectangIe& , PoIy&) ; Fire (EIIipse&, EIIipse&) ; Fire (EIIipse&, PoIyD ; Fire (P01y&, P01y&) ;
302 ⅵ代 u Rectangle 第 11 章 Shape ⅵ杙 u 引 RoundedShape マルチメソッド RoundedRectangle 図 11.2 仮想継承を用いたダイアモンド型のクラス階層 またも問題に遭遇してしまいました。以下のコードを考えてみましよう : ているのか判定することができないのです。 RoundedRectang1e& に static-cast しようとしても , コンパイラはどちらの Shape 部分オプジェクトを指し しているのでしようか , それとも Rectang1e 中の Shape を指しているのでしようか ? 同様に , Shape& を Shape への変換が曖昧になることを意味しています。こういった変換は , RoundedShape 中の Shape を指 は , 2 つの異なった s ape 型の部分オプジェクトを保持しているのです。これは , RoundedRectang1e から クラス階層の形は同じものの , オプジェクトの構造は大きく異なっています。この場合の RoundedRectang1e 図 11.3 は , この階層を図で表現したものです。 public RoundedShape { class RoundedRectang1e : public Rectang1e , class RouondedShape : public Shape { class Rectang1e : public Shape { class Shape { 次に , 仮想継承を用いず , 通常の多重継承のみを用いた類似のクラス階層を分析してみましよう : ならないのです。 てもうまく動作するのです。要するに , 仮想継承を用いた階層が存在する場合 , dynamic-cast を用いなければ しかし , dynamic-cast はクラス間の関係を取得する際 , さらに進んだ手段を用いるため , 仮想基底が存在し 底オプジェクトから導出した型へは st at ic-cast することができないのです。 導出されたオプジェクトに戻すコンパイル時のメカニズムは存在しないということになります。つまり , 仮想基 ことを考えた場合 , 導出された型のオプジェクトを , いったん仮想基底型にキャストしてしまうと , それを元の しかし , 基底オプジェクトには , 導出されたオプジェクトへのポインタが格納されていないのです。こういった ます。導出された型から基底型へのキャストが発生した場合 , コンパイラはそのポインタを使用するわけです。 多重継承の実装によっては , 導出された各オプジェクト中に , その基底オプジェクトへのポインタが格納され RoundedRectang1e roundRect ; Rectang1e& rect = roundRect ; Shape& shapel = rect ; / / 曖昧でない暗黙の変換
288 のメリットは , 速度 ( 階層中に多くの型が存在しない場合 ) と非侵入性です。 には , 階層の修正を行う必要がないのです。 11.5 カまかせのディスパッチャにおける対称性 第 11 章マルチメソッド StaticDispatcher を用いる場合 2 つの図形の交差領域を網掛けする際 , 四角形が楕円に重なっている状態と楕円が四角形に重なっている状態 を区別する必要のある場合があります。それとは逆に , 楕円と四角形の交差領域を , 重なっている状態とは関係 なく , 同じように網掛けする必要のある場合もあります。後者の場合に必要となるのが , 引き渡した引数の順序 を意識しないマルチメソッド , すなわち対称型マルチメソッドです。 対称性は , 2 つのパラメータ型が同一である ( 私たちの例では BaseLhs が BaseRhs と同じであり , TypesLhs が TypesRhs と同じである ) 場合にのみ適用されます。 こまでで定義したカまかせの StaticDispatcher は , 非対称 , つまり , 対称型マルチメソッドの組み込み サポートを提供していません。例えば , 以下のクラスを定義したと考えて下さい・ class HatchingExecutor public : void Fire (RectangIe& , RectangIe&) ; void Fire (RectangIe& , EIIipseD ; ー・ / 、ンドラ / / 工ラ void OnError (Shape& , Shape&) ; typedef StaticDispatcher く HatchingExecutor , Shape , TYPELIST_3(Rectang1e , E11ipse , P01y) HatchingDispatcher ; この HatchingDispatcher に対して , 左手側のパラメータとして EIIipse, 右手側のパラメータとして Rectang1e を引き渡した場合 , Fire は呼び出されません。 HatchingExecutor の観点から見た場合 , どちらが 先でどちらが後かは関係ないものの , HatchingDispatcher は特定の順序でオプジェクトを引き渡すことを要 求するのです。 逆順の引数でオーバーロードを行い , 適切な関数に転送することによって , 利用者側のコードで対称性を作り 出すことは可能です : class HatchingExecutor public : void Fire (Rectang1e& , E11ipse&) ; / / 対称性の保証 void Fire (E11ipse& lhs , Rectang1e& rhs)
11.4 カまかせのアプローチを自動化する 287 るわけです。このルーチンには , 何でも適切なものを記述することができます。これが呼び出された場合 , StaticDispatcher が動的な型を発見できなかったということなのです。 以前のセクションで考察したように , カまかせのディスパッチャには継承関係に端を発する問題が存在しま す。つまり , 以下の StaticDispatcher の実体化にはバグが存在するのです : typedef StaticDispatcher SomeExecutor , Shape , TYPELIST_4(Rectang1e , E11ipse , P01y, RoundedRectang1e) MyDispatcher ; MyDispatcher に RoundedRectang1e を引き渡した場合 , それは Rectang1e として扱われます。このため , RoundedRectang1e へのポインタに対する dynamic-cast く Rectang1e*> が成功してしまい , 食物連鎖の下位層 にある dynamic-cast く RoundedRectang1e*> には適合の機会が与えられないのです。正しい実体化は以下のよ うになります : typedef StaticDispatcher SomeExecutor , Shape , TYPELIST_4(RoundedRectang1e , E11ipse , P01y, Rectang1e) Dispatcher; 一般規則は , 継承階層で最も下位にあるものをタイプリストの先頭に持ってくることです。 こういった変形が自動的にできれば便利でしよう。ということで , タイプリストはそういった機能をサポート しています。私たちはコンパイル時に継承階層を検出する手段を持っているため ( 第 2 章 ) , タイプリストを並 べ替えることもできるのです。これを可能にするコンパイル時のアルゴリズム DerivedToFront は , 第 3 章で 取り扱っています。 つまり , 自動的な並べ替えを行うには , StaticDispatcher の実装を以下のように修正するだけなのです : template く . class StaticDispatcher typedef typename DerivedToFront く typename TypesLhs : :Head> : : Resu1t Head; typedef typename DerivedTOFront く typename TypesLhs : :Tai1> : : Resu1t Tai1 ; public : 以前と同じ こういった手軽な自動化で達成できたのは , コード生成という部分のみである点を忘れてはいけません。依存 性の問題は , まだ残っています。しかし , StaticDispatcher には階層中の全ての型に対する依存性が保持さ れているものの , カまかせのアプローチによるマルチメソッドの実装は , 非常に簡単なものになったのです。そ
314 11.14 ダブル・ディスパッチャのまとめ 第 11 章マルチメソッド ・ Loki には , StaticDispatcher, BasicDispatcher, BasicFastDispatcher という 3 つの基本的なダブ ル・ディスパッチャが定義されています。 ・ StaticDispatcher の宣言は以下の通りです : template く class Executor, class BaseLhs , class TypesLhs , class BaseRhs = BaseLhs , class TypesRhs = TypesLhs , typename Resu1tType = void class StaticDispatcher; ー BaseLhs は , 左手側の基底型です。 ー TypesLhs は , ダブル・ディスパッチを行う左手側の具体的な型を保持したタイプリストです。 ー BaseRhs は , 右手側の基底型です。 —TypesRhs は , ダブル・ディスパッチを行う右手側の具体的な型を保持したタイプリストです。 ー Executor は , 型の探索後に起動される関数を提供するクラスです。 Executor は , TypesLhs 中およ び TypesRhs 中の各型の組み合わせに対してオーバーロードした Fire メンバ関数を提供しなければな りません。 ー Resu1tType は , オーバーロードした Executor: :Fire 関数の戻り型です。戻り値は , StaticDispatcher: :Go の結果として転送されます。 ・ Executor は , 工ラーの取り扱いを行う OnError(BaseLhs&, BaseRhsD メンバ関数を提供しなければな りません。 StaticDispatcher は , 未知の型に遭遇した場合 Executor : :OnError を呼び出します。 例 (Rectang1e と E11ipse が Shape を継承し , Printer と screen が 0utputDevice を継承していると 仮定して下さい ) : struct Painter b001 b001 b001 b001 b001 typedef く Fire (Rectang1e& , Printer&) ; Fire (E11ipse&, Printer&) ; Fire (Rectang1e&, ScreenD ; Fire (E11ipse& , Screen&) ; OnError(Shape& , OutputDevice&) ; StaticDispatcher Painter,
11.9 引数の変換 : static-cast か dynamic-cast か ? 301 Dispatcher disp; disp. Add く Rectang1e , poly> (HatchRectang1eP01y() ) ; disp. Add く E11ipse , Rectang1e> (HatchE11ipseRectang1e ( ) ) ; です。 1 つは , 仮想継承を用いている場合に発生します。以下のクラス階層を考えてみて下さい・ しかし , static-cast ではうまくいかず , 頼れるのは dynamic ー cast だけであるというケースが 2 つあるの dy Ⅱ amic ー cast で正当性をチェックすることは無駄に思えるかもしれません。 が判っているはずです。このため , 単純な static ー cast が同じ結果をずっと短い時間で達成できるのであれば , して , それを実装するメカニズムによって , ダブル・ディスパッチャもマップ中に存在するエントリの実際の型 関数やファンクタの登録時には , それが特定の既知の型に対して呼び出されることが判っているはずです。そ の安全性は , 実行時の効率を犠牲にしたものなのです。 今までのコードは全て , 安全な dynamic-cast を用いてキャストを実行していました。しかし , dynamic-cast 11 9 引数の変換 : static-cast か dynamic-cast か ? ReverseAdapter オプジェクトが定義されるのです。 似ています。 FunctorDispatcher : : Add によって , キャストを行って呼び出し順序を逆転させる新たな FunctorDispatcher に対する対称性の実装は , FnDispatcher に対して対称性を実装する場合と良く は , 取り扱う型に対して。 perat 。 r ( ) を実装するだけなのです。 2 つのファンクタには , ( 共通の基底から継承されているといったような ) 関係は何もありません。必要なこと public RoundedShape { class RoundedRectang1e : public RectangIe , class RouondedShape : virtual public Shape { class Rectang1e : virtual public Shape { class Shape { イアウトに付加していくような方法で継承を処理することができないのです。 を提供するものです。この場合 , コンパイラは , サプクラスが追加するものを , 基底オプジェクトのメモリ・レ からです。仮想継承とは , 継承元にある複数のクラスが同一の基底クラス・オプジェクトとして共有される手段 パイル時にエラーが発生するのです。その理由は , 仮想継承と通常の継承が , まったく違った動作となっている Shape& を Rectang1e&, RoundedShape&, RoundedRectang1e& のいずれかにキャストしようとした途端 , コン ディスパッチャは , 今のままでも正しく動作します。しかし dynamic-cast を static-cast に置き換え , たダブル・ディスパッチャは , ダイアモンド型のクラス階層でも動作しなければならないのです。 は多くの欠点があるにも関わらず , それが必要となる状況は確実に存在するのです。このため , 私たちが定義し は , 利用者が何を行いたいのか , あなたには絶対判らないということなのです。ダイアモンド型のクラス階層に これは良いクラス階層とは言えないかもしれませんが , クラス・ライプラリを設計する際に判っていること 図 11.2 は , この階層におけるクラス間の関係を図で表現したものです。
282 第 11 章マルチメソッド void Doub1eDispatch(Shape& 1 れ s , Shape& rhs) if (typeid(lhs) = = typeid(Rectang1e) ) Rectang1e* pl = dynamic-cast く Rectang1e*> ( & 1 s ) ; else これによって , 導出された型に対して誤判定が行われることは無くなります。このコードのように typeid を 用いて比較すると , lhs が RoundedRectangIe である場合は判定が成功しないため , 判定処理は続行されるわ けです。そして , 最後には typeid(RoundedRectang1e) に対する判定が成功するのです。 ところがこういった一面のみを修正すると , 他の部分にほころびが生じるのです。つまり Doub1eDispatch は , 柔軟性の無いものになってしまうのです。元々のコードでは , D 。 ub1eDispat 曲中に型の判定を追加しな かった場合 , その型に最も近い基底型で処理を行わせることができます。継承を用いるということは , 通常はこ ういったことを期待しているはずなのです。導出されたオプジェクトというものは , 何らかの動作がオーバーラ イドされていない限り , 基底オプジェクトと同じ動作を行うものなのです。 Doub1eDispatch を typeid に基づ こういった属性が維持されなくなってしまうという問題が出てくるわけです。この事実から導 いて実装すると , き出せる答えは , Doub1eDispatch 中で dynamic-cast を用い続け , 最も下位にあるクラスが最初に判定され るよう , if の判定を「ソートする」ことでしよう。 これによって , マルチメソッドをカまかせに実装することに対して , 2 つの欠点が追加されます。まず , Doub1eDispatch が Shape の階層関係に依存するという点です。 Doub1eDispatch は , クラスについてだけで はなく , クラス間の継承関係も知っていなければならないのです。そしてもう 1 つは , ダイナミック・キャスト が適切な順序となるよう保守しなければならないため , 保守作業者の重荷が増えるという点です。 11 4 カまかせのアプローチを自動化する カまかせのアプローチによって達成される速度が , どうしても必要となる状況もあり得るはずです。このた め , こういったディスパッチャの実装を行っておく価値はあるでしよう。ここはタイプリストの出番です。 第 3 章では , Loki が定義しているタイプリスト機能ーっまり , 型のコレクションを操作可能にする言語構造と コンパイル時のアルゴリズムを解説していたことを思い出して下さい。マルチメソッドをカまかせに実装する 場合 , 階層中の型 ( 今の例では Rectang1e, poly, E11ipse 等 ) は , タイプリストの形で提供することになるで しよう。そして , if-else ステートメントの羅列は , 再帰的なテンプレートを用いて生成することになります。 一般的なケースでは , 異なった型のコレクション同士によってディスパッチが行われる可能性も考えられるた め , 左手側オペランドのタイプリストと右手側オペランドのタイプリストは , 異なったものを与えられるように します。 それでは , 型の推論アルゴリズムを実行し , 別クラスにある関数を呼び出す StaticDispatcher クラス・テ ンプレートの概略を考えてみましよう。解説はコードの後にあります : template く
11.3 型の判定をニ重化する : カまかせに行う方法 if (Rectang1e* p2 = dynamic—cast く Rectang1e*> (&rhs) ) DoHatchArea3(*p2 , *pl) ; else if (E11ipse* p2 = dynamic-cast く E11ipse*>(&rhs)) DoHatchArea4(*p2 , *pl) ; else if (P01y* p2 = dynamic—cast く P01y*> (&rhs) ) DoHatchArea6(*p1 , *p2) ; else Err 。 r ( " 未定義の交差 " ) ; else E てて。 r ( " 未定義の交差 " ) ; 281 うーむ , 結構な行数になってしまいました。見ていただいたように , カまかせのアプローチでは , 多くのコー ド ( 単純ではありますが ) を記述する必要があります。こういった if ステートメントを適切に組み合わせたも のを記述するよう , 信頼できる C+ + プログラマーに頼むことはできるでしよう。さらにこの解決策は , クラスの 組み合わせ数がさほど多くなければ , 高速に動作できるという利点もあります。速度という観点から見た場合 , Doub1eDispatch は可能な組み合わせ集合の中から , 線形探索を実装していることになります。探索処理がルー プではなく , if-else ステートメントの羅列として展開されているため , 集合が小さい場合 , 非常に速い探索速 度となるわけです。 このカまかせのアプローチにおける問題は , コードのサイズが大きくなり , クラスの数が増えるに従ってコー ドが保守できなくなってしまうという点にあります。上記のコードは , たった 3 つのクラスの場合なのですが , それでも結構な大きさとなってしまうのです。そして , クラスを追加するとサイズは指数的に大きくなっていき ます。階層中のクラス数が 20 個あった場合 , DoubIeDispatch のコードがどれくらい大きくなるか考えてみて 下さい ! もう 1 つの問題は , D 。 ub1eDispatch が依存性によるポトルネックを生み出しているという点にあります。っ まりこの実装は , 階層中に存在する全クラスを知っていなければならないのです。依存性は可能な限り少ない方 が良いのですが , Doub1eDispatch は多くの依存性を抱えてしまっているのです。 Doub1eDispatch を用いた場合のさらなる問題は , if ステートメントの順序が処理に影響を与えてし まうという点にあります。これは油断ならない非常に危険な問題です。例えば , RectangIe ( 四角形 ) か ら RoundedRectang1e ( 角を丸く面取りした四角形 ) というクラスを導出したと考えて下さい。その後 , Doub1eDispatch を編集し , 各 if-else ステートメントの末尾にある Error 呼び出しの直前に , 追加の if ス テートメントを挿入したとします。これによってバグが生まれるのです。 その理由は , RoundedRectang1e へのポインタを Doub1eDispatch に引き渡した場合 , dynamic-cast く Rectang1e*> が成功してしまうからです。この判定は , dynamic-cast く RoundedRectangIe*> の前に存在 するため , 最初の判定によって Rectang1e と RoundedRectang1e の双方のケースが「食べられて」しまうわけ です。 2 番目の判定は適合する機会が無いのです。ほとんどのコンパイラはこういったことに関して警告を与え てくれません。 考えられる解決策としては , 判定を以下のように変更することでしよう :
290 / / 工ラー・ハンドラ void OnError(Shape&, Shape&) ; 第 11 章 マルチメソッド こで考察したアルゴリズムによって除去された組み合わせ , すなわち E11ipse- StaticDispatcher は , Rectang1e, P01y-Rectang1e, poly ーを E11ipse を全て自身で検出しなければなりません。そして , この 3 つ の組み合わせに対して引数を逆向きにしなければならないわけです。その他全ての組み合わせに対しては , 今ま で通り呼び出しを転送するだけとなります。 引数の交換が必要となるかどうかを決定するプーリアン条件は何でしようか ? このアルゴリズムは , Types s の型インデックスが TypesLhs の型インデックスよりも小さい場合にのみ選択されます。従って , 条件は以下の ようになります : 2 つの型 T と U に対して , TypesRhs 中における U のインデックスが TypesLhs 中における T のインデック スよりも小さい場合 , 引数は交換しなくてはならない。 例えば , T が E11ipse であり , U が Recta Ⅱ gle であるとします。この場合 , TypesLhs 中における T のインデッ クスは 1 であり , Types s 中における U のインデックスは 0 となります。このため , E11ipse と Rectang1e は Execut 。 r : : Fi て e を起動する前に交換しなければならないと正しく判定できるわけです。 タイプリスト機能には , タイプリスト中の型の位置を返すコンパイル時のアルゴリズムとして 1ndexOf が既 に提供されています。このため , 交換条件は簡単に記述することができるでしよう。 まず , ディスパッチャが対称型であるかどうかを指定する新たなテンプレート・パラメータを追加しなければ なりません。その後 , Executor::Fire メンバ関数を呼び出す際に , 必要に応じて引数の交換を行う , 簡単な exec . Fire(1hs, rhs) ; Executor& exec) static void DoDispatch(SomeLhs& lhs , struct 工 nvocationTraits template く b001 swapArgs , class SomeLhs , class StaticDispatcher typename Resu1tType = void class TypesRhs = TypesLhs , class BaseRhs = BaseLhs , class TypesLhs , class BaseLhs , b001 symmetric , class Executor, く template InvocationTraits クラス・テンプレートを追加します。以下がその内容です : SomeRhs& rhs , class SomeRhs> template く class SomeLhs , class SomeRhs>