squirrel_code @ ウィキ
続続 pImpl × 継承
最終更新:
squirrel_code
-
view
続続 pImpl × 継承
last update: 2010/12/28 (Tue)
続 pImpl × 継承 の続き
その4では,private 継承を用いて pImpl の継承をある程度スマートに実現できた.これをさらに推し進めてみる.
その4で唯一面倒なのが, private スコープに展開された InterfaceImpl のメソッド群を public スコープに輸出してやらなければならない点だ.これは DerivedInterface を継承するクラスが増えてくるとなかなか面倒な作業となり,バグも発生しやすい.
ここで,この種の定型作業をこなすために, C++ にはテンプレートがある.この「メソッドの輸出」をテンプレート化できないだろうか.
pImpl の継承(その5)
「メソッドの輸出」テンプレートが満たすべき条件は何だろう. DerivedInterfaceImpl の public なスコープに干渉するものであるから,継承ツリーの中で DerivedInterface と DerivedInterfaceImpl の間に挟まるものでなければならない.ここから DerivedInterface をテンプレートパラメータに持たなくてはならないことがわかる.
次に輸出するメソッドの実装を持たなければならないから,InterfaceImpl を protected 継承していなければならない.そのぐらいだろうか.書いてみる.
// pImpl.h #include <string> // インターフェイス class Interface { public: virtual ~Interface() {} // Interface のメソッド群 virtual const int getID() const = 0; virtual void setID(const int id) = 0; // ファクトリメソッド static Interface* getInstance(); }; // 派生インターフェイス class DerivedInterface : public Interface { public: virtual ~DerivedInterface() {} // DerivedInterface のメソッド群 virtual const std::string getName() const = 0; virtual void setName(const std::string& name) = 0; // ファクトリメソッド static DerivedInterface* getInstance(); }; // pImpl.cpp #include "pImpl.h" // インターフェイスの実装 class Impl : public Interface { int _id; public: Impl() {} ~Impl() {} const int getID const; void setID(const int id); }; const int Impl::getID() const { return _id; } void Impl::setID(const int id) { _id = id; } // メソッド輸出用テンプレート template <typename T> class ImplMixin : public T, protected Impl { public: ImplMixin() : Impl() {} virtual ~ImplMixin() {} virtual const int getID() const { return Impl::getID(); } virtual void setID(const int id) { Impl::setID(id); } }; // Interface のファクトリの実装 Interface* Interface::getInstance() { return new Impl(); } // 派生インターフェイスの実装 class Derived : public ImplMixin<DerivedInterface> { std::string _name; public: Derived() : ImplMixin<DerivedInterface>() {} ~Derived() {} const std::string getName() const { return _name; } void setName(const std::string& name) { _name = name; } }; // DerivedInterface のファクトリの実装 DerivedInterface* DerivedInterface::getInstance(); { return new Derived(); }
できてしまった(汗)今回はわかりやすさのためメソッドに意味を持たせ,また名前を短くしている.さらに,ファクトリは静的メソッドにしたほうがスマートに思えたのでそうした.
一応確認したが,ちゃんと Impl クラスのコンストラクタ,デストラクタも呼ばれている.ここでも,もし Interface がインスタンス化される実装クラスを持たないならば, ImplMixin テンプレートに Impl の実装を移せば Impl は要らなくなる.これはいわゆる「テンプレートによる Mixin 」と同じものである.
また getID と setID が派生クラスでオーバーライドされることを考慮して ImplMixin で virtual にしてあるが,オーバーライドされない場合は virtual にしなくてよい.
結局どういうことなのか
インターフェイスと実装の分離パターンを分類し,クックブック化しておく.
パターンP
- インターフェイスはそもそも継承されない.
- 従って,インスタンス化される実装クラスを持つ.
- 即値での代入・コピーを行う.
通常の pImpl.
パターンA
- 基底インターフェイスは実装クラスを持たない.
- 派生インターフェイスはインスタンス化される実装クラスを持つ.
派生インターフェイスは基底インターフェイスを public 継承,派生実装クラスは派生インターフェイスを public 継承.
パターンB
- 基底インターフェイスは実装クラスを持つが,インスタンス化されない.
- 派生インターフェイスはインスタンス化される実装クラスを持つ.
派生インターフェイスは基底インターフェイスを public 継承,派生インターフェイスをテンプレートパラメータに持ち,テンプレートパラメータを public 継承し基底インターフェイスを継承しない基底実装テンプレートを定義,派生実装クラスはテンプレート引数に派生インターフェイスを指定した基底実装テンプレートを public 継承.
パターンC
- 基底インターフェイスはインスタンス化される実装クラスを持つ.
- 派生インターフェイスはインスタンス化される実装クラスを持つ.
- 派生インターフェイスはさらに派生しない
派生インターフェイスは基底インターフェイスを public 継承,基底インターフェイスを public 継承する基底実装クラスを定義,派生インターフェイスをテンプレートパラメータに持ち,テンプレートパラメータを public 継承し,基底実装クラスを private 継承する基底クラスメンバ輸出用テンプレートを定義,派生実装クラスはテンプレート引数に派生インターフェイスを指定した輸出用テンプレートを public 継承.
パターンD
- 基底インターフェイスはインスタンス化される実装クラスを持つ.
- 派生インターフェイスはインスタンス化される実装クラスを持つ.
- 派生インターフェイスはさらに派生する.
派生インターフェイスは基底インターフェイスを public 継承,基底インターフェイスを public 継承する基底実装クラスを定義,派生インターフェイスをテンプレートパラメータに持ち,テンプレートパラメータを public 継承し,基底実装クラスを protected 継承する基底クラスメンバ輸出用テンプレートを定義(輸出するメソッドは virtual 指定),派生実装クラスはテンプレート引数に派生インターフェイスを指定した輸出用テンプレートを public 継承.
いかがだっただろうか.C++ の多重継承やテンプレートは複雑だといわれるが,Java の interface 機構や Generics とは別モノだと考えたほうがよいと思う.確かにとっつき難い点はあるにしても,ちゃんと使えば,例えば Java ではダウンキャストの嵐になるような場面でもシンプルな実装が可能になる.
結論 : 多重継承とテンプレートを怖がるな^^
&trackback()
参考
コメント
- コメントの投稿テスト -- (tossy_squirrel) 2010-12-29 03:35:18