More C++ Idioms/初期化中の仮想関数呼び出し(Calling Virtuals During Initialization)
初期化中の仮想関数呼び出し (Calling Virtuals During Initialization)
編集
意図
編集オブジェクトの初期化中に仮想関数の呼び出しをシミュレートする。
別名
編集初期化中の動的束縛イディオム(Dynamic Binding During Initialization idiom)
動機
編集派生クラスのオブジェクトを初期化中に仮想関数を呼び出したいときがある。 言語規格では、これを明示的に禁止している。なぜなら、オブジェクトの派生クラス相当部分が 初期化される前に、派生クラスのメンバ関数を呼び出すことが危険だからである。 仮想関数が生成中のデータメンバにアクセスしない場合、これは問題にならない。 しかし、これを保証する一般的な方法がない。
class Base {
public:
Base();
...
virtual void foo(int n) const; // しばしば純粋仮想関数
virtual double bar() const; // しばしば純粋仮想関数
};
Base::Base()
{
... foo(42) ... bar() ...
// これらの呼び出しは動的束縛されない。
// 目標: これらの呼び出しにおいて動的束縛をシミュレートする。
}
class Derived : public Base {
public:
...
virtual void foo(int n) const;
virtual double bar() const;
};
解法とサンプルコード
編集やりたいことを実現する方法は複数ある。 それぞれに、利点と欠点がある。 一般的に、これらの方法は 2 つに分類できる。 一つは、二段階初期化(two phase initialization)を用いるものであり、 他方は、一段階初期化(single phase initialization)のみを用いるものである。
二段階初期化技法は、オブジェクトの状態初期化から、オブジェクトの生成を分離するものである。 そのような初期化はいつでも可能とは限らない。 オブジェクトの状態初期化は単独の関数に分離される。この関数はメンバ関数にも、 フリー関数(非メンバ関数)にもなりうる。
class Base {
public:
void init(); // 仮想関数かもしれないし、そうではないかもしれない
...
virtual void foo(int n) const; // しばしば純粋仮想関数
virtual double bar() const; // しばしば純粋仮想関数
};
void Base::init()
{
... foo(42) ... bar() ...
// ほとんどはオリジナルの Base::Base() からのコピー
}
class Derived : public Base {
public:
Derived (const char *);
virtual void foo(int n) const;
virtual double bar() const;
};
- 非メンバ関数の使用
template <class Derived, class Parameter>
std::auto_ptr <Base> factory (Parameter p)
{
std::auto_ptr <Base> ptr (new Derived (p));
p->init ();
return p;
}
これが非テンプレート版によるこのアプローチの例である。 factory 関数は、基本クラス内に移動することもできるが、 静的関数である必要がある。
class Base {
public:
template <class D, class Parameter>
static std::auto_ptr <Base> Create (Parameter p)
{
std::auto_ptr <Base> ptr (new D (p));
p->init ();
return p;
}
};
int main ()
{
std::auto_ptr <Base> b = Base::Create <Derived> ("para");
}
クラス Derived のコンストラクタは、使用者が誤って使わないように private にするべきである。 インタフェースは正しく使うことが簡単で誤って使うことが難しくなっているべきである点に留意せよ。 その上で、factory 関数は派生クラスの friend とするべきである。 メンバ関数による生成を行う場合には、Base クラスは、Derived クラスの friend とすることができる。
- 二段階初期化を使用しない場合
補助的な階層を用いることでやりたいことを実現する方法が、[1]に記載されている。 しかし、余分なクラス階層を維持することは望ましいことではない。 また、ポインタを静的メンバ関数に渡すのは C 風である。 この状況では、奇妙に再帰したテンプレートパターンイディオム(Curiously Recurring Template Pattern idiom)が有用になりうる。
class Base {
};
template <class D>
class InitTimeCaller : public Base {
protected:
InitTimeCaller () {
D::foo ();
D::bar ();
}
};
class Derived : public InitTimeCaller <Derived>
{
public:
Derived () : InitTimeCaller <Derived> () {
cout << "Derived::Derived()\n";
}
static void foo () {
cout << "Derived::foo()\n";
}
static void bar () {
cout << "Derived::bar()\n";
}
};
メンバによる基本クラスの初期化イディオム(Base-from-Member idiom)を用いることで、 このイディオムのより複雑な変種ができる。