beakmarkさんがブックマークされていたのを何気なく読んでいたら、いつものように自分ならどうするか、と考え出してしまったので、その経緯と結果のメモ*1。
まず前半の話。
クラス関数やクラス変数へのアクセスであれば型と名前さえわかっていればアクセスできるので、「奇妙に再帰したテンプレートパターン(Curiously Recurring Template Pattern)*2」を使ってできるんじゃないかとまず考えました。加えて今回の例では実装の継承とインタフェースの継承に関連がないので、それぞれを分離。
#include <iostream> class Fruit // インタフェースの基底クラス { public: Fruit() {} virtual ~Fruit() {} }; template<typename T> class Preconstruction // 実装の再利用のための基底クラステンプレート { public: Preconstruction() { std::cout << "私は" << T::name() << "です。\n"; } }; // あくまで一般論ですが、実装の基底クラスはprivateで継承 // (実装の再利用が目的なので、外部に見せる必要がないため) class Apple : public Fruit, private Preconstruction<Apple> { public: static const char* name() { return "りんご"; } }; class Banana : public Fruit, private Preconstruction<Banana> { public: static const char* name() { return "バナナ"; } }; int main(int, char* []) { Apple a; Banana b; return 0; }
実行結果。
私はりんごです。 私はバナナです。
次に後半の話。
こちらは、派生クラスの要素を基底クラスのコンストラクタから操作したいという要求を解決する、というもの。内容を整理してみると「派生クラスの要素への操作をまとめたい」という要求と「共通する操作だから基底クラスに操作をまとめたい」という要求によるものが今回の動機ということになると思います。
このことだけを取り出すと、つまりこれも「実装の継承」をやりたいということになると思います。
ならば、とくに継承関係によって操作をまとめる必要はなくて全く別のクラスによって操作をしてもいいはずと考えました。
で、Preconstruction
クラスを導入してみる。ポイントはPreconstruction
のオブジェクトを派生クラスの最後のメンバ変数として宣言しているところ。C++ではメンバ変数の初期化は宣言順に行われるため、最後に宣言されるメンバ変数はそれより前に宣言されているメンバ変数を参照することができます(それらはすでに初期化されているので)。
#include <iostream> #include <string> class Fruit { public: Fruit() {} virtual ~Fruit() {} virtual const std::string& name() const = 0; virtual const std::string& color() const = 0; }; struct Preconstruction { Preconstruction(Fruit* fruit) { std::cout << "私は" << fruit->name() << "です。色は" << fruit->color() << "です。\n"; } }; class Apple : public Fruit { public: Apple(const std::string& color) : color_(color), preconstruction_(this) {} const std::string& name() const { static const std::string apple("りんご"); return apple; } const std::string& color() const { return color_; } private: const std::string& color_; Preconstruction preconstruction_; }; int main(int, char* []) { Apple a("赤"); Apple b("青"); return 0; }
実行結果。
私はりんごです。色は赤です。 私はりんごです。色は青です。
ただし。C++の仕様で決められているとはいえ、いささか危なっかしい感じです。宣言の順序間違えたら終りですし。あとPreconstruction
の分だけオブジェクトのサイズが大きくなるのもなんかイヤ。
そもそもの動機が、派生クラスを使う側で生成と初期化を分けてする(二段階になる)のがいやだというとこころですから、初期化の実行を派生クラスのコンストラクタのなかでやってしまうということも考えられると思います。
#include <iostream> #include <string> class Fruit { public: Fruit() {} virtual ~Fruit() {} virtual const std::string& name() const = 0; virtual const std::string& color() const = 0; protected: void init(Fruit* fruit) { std::cout << "私は" << fruit->name() << "です。色は" << fruit->color() << "です。\n"; } }; class Apple : public Fruit { public: Apple(const std::string& color) : color_(color) { init(this); } const std::string& name() const { static const std::string apple("りんご"); return apple; } const std::string& color() const { return color_; } private: const std::string& color_; }; int main(int, char* []) { Apple a("赤"); Apple b("青"); return 0; }
実行結果。
私はりんごです。色は赤です。 私はりんごです。色は青です。
これもなんかイヤという感覚もわからなくもないですが。
ただ、C++では初期化は必ず「基底クラス→派生クラス」の順に行われるので、基底クラスのコンストラクタの中で派生クラスのインスタンスメンバにアクセスするのは絶対にやっちゃまずいこと。なのでどうしても派生クラス(のコンストラクタ)の中でどうにかしてやるしかないわけで、これはC++ではしかたがないのかなぁ、と。