More C++ Idioms/仮想コンストラクタ(Virtual Constructor)
仮想コンストラクタ(Virtual Constructor)
編集
意図
編集あるオブジェクトのコピーか、新しいオブジェクトを、その具体的な型を知ることなしに生成する。
別名
編集初期化におけるファクトリメソッドの使用
動機
編集クラス階層中のメンバ関数の多態的な呼び出しを使うことは、オブジェクト指向プログラミングのコミュニティではよく知られたことである。 それは、is-a(~は~である) 関係 (より現実的に言えば behaves-as-a(~として振る舞う)関係)を実装する方法の一つである。 クラス階層中の生存期間管理(生成、コピー、破棄)関数を多態的に呼び出すことも場合によっては便利である。
C++ は、仮想デストラクタによってオブジェクトの多態的な破棄に(言語組み込みの機能で)対応しているが、 オブジェクトの生成やコピーに対しては同様のものは存在しない。 C++ では、オブジェクトの生成には常にその型をコンパイル時に知っている必要がある。 仮想コンストラクタ(Virtual Constructor)イディオムは、C++ で多態的なオブジェクトの生成やコピーを可能とする。
解法とサンプルコード
編集仮想コンストラクタはオブジェクトの生成に create() メンバ関数を用い、コピー生成に clone() メンバ関数を用いて以下のように書ける。
class Employee
{
public:
virtual ~Employee () = default; // C++組み込みの多態的破棄
[[nodiscard]] virtual auto create () const -> Employee * = 0; // 仮想コンストラクタ(生成)
[[nodiscard]] virtual auto clone () const -> Employee * = 0; // 仮想コンストラクタ(コピー)
};
class Manager : public Employee // "is-a" 関係
{
public:
Manager (); // デフォルトコンストラクタ
Manager (Manager const &); // コピーコンストラクタ
~Manager () override = default; // デストラクタ
[[nodiscard]] auto create () const override -> Manager * // 仮想コンストラクタ(生成)
{
return new Manager();
}
[[nodiscard]] auto clone () const override -> Manager * // 仮想コンストラクタ(コピー)
{
return new Manager (*this);
}
};
class Programmer : public Employee { /* Manager クラスとほとんど同様 */ };
auto duplicate (Employee const & e) -> Employee *
{
return e.clone(); // 仮想コンストラクタイディオムの使用。
}
Manager クラスは 2 つの純粋仮想関数を実装し、型名(Manager)を用いてその型のオブジェクトを生成する。 duplicate 関数は、どのように仮想コンストラクタイディオムが使用されるかを示している。 duplicate 関数は、実際にはなにを複製しているかを知らない。 (実際には Manager かもしれないし Programmer かもしれない) Employee を複製していることを知っているだけである。 正しいインスタンスを生成する責任は派生クラスに委譲されている。 それゆえ、 Employee を頂点とするクラス階層に将来さらに派生クラスが追加されたとしても、duplicate 関数は変更に対して影響を受けない。
Manager クラスの clone および create メンバ関数の返値の型は Employee ではなく、そのクラス自身(Manager)である。 C++ は、派生クラスが関数をオーバーライドするとき、返値の型を基本クラスの関数の返値の型の派生型とすることができる。 この言語機能は、共変の返値型(co-variant return types)として知られている。
リソースの所有権を正しく扱うためには、clone() および create() 関数をファクトリ関数とみなし、その返値に対してリソースの返値(Resource Return)イディオムを利用すべきである。 しかし、返値の型は shared_ptr<Employee> と shared_ptr<Manager> のようになり、もはや共変の返値型ではなくなりコンパイルに失敗するはずである。 そのような場合では、派生クラスの仮想コンストラクタ関数は親クラスと正確に同じ型を返さなければならない。
#include <tr1/memory>
class Employee
{
public:
using Ptr = std::tr1::shared_ptr<Employee>;
virtual ~Employee () = default; // C++組み込みの多態的破棄
[[nodiscard]] virtual auto create () const -> Ptr = 0; // 仮想コンストラクタ(生成)
[[nodiscard]] virtual auto clone () const -> Ptr = 0; // 仮想コンストラクタ(コピー)
};
class Manager : public Employee // "is-a" 関係
{
public:
Manager () = default; // デフォルトコンストラクタ
Manager (Manager const &) {} // コピーコンストラクタ
~Manager () override = default;
[[nodiscard]] auto create () const override -> Ptr // 仮想コンストラクタ(生成)
{
return Ptr(new Manager());
}
[[nodiscard]] auto clone () const override -> Ptr // 仮想コンストラクタ(コピー)
{
return Ptr(new Manager (*this));
}
};