クラスにメンバ変数を持たせるには
struct Name {
Type member;
};
これが普通ですが時には不満が出てきます。例えばメンバ変数の生成タイミングを遅らせたいなど、クラス寿命の範囲内でメンバ変数の寿命を管理したい場合や、#include する Type の定義が重すぎてコンパイル時間が延びるので PImplイディオム を使用してヘッダ間の依存性を減らしたい場合、またはクラスの定義が循環していてコンパイルできない場合などに不完全型のメンバを持ちたい場合です。
寿命管理を実現したい場合
まず寿命管理と聞いて思い浮かぶのがstd::unique_ptrやstd::shared_ptrですがそれぞれ問題があります。まずunique_ptrですがこれをメンバに持つとクラスがコピーできなくなってします。これを不満に思う人は結構いると思います。これはコピーコンストラクタやコピー代入演算子を自分で定義すれば解決できますが、デストラクタをRAIIに任せて自分でなるべく書くべきではないのと同じくこれらの関数も自分で書くのは間違いや保守性の低下の元ですから避けたいところです(代入演算子については自分で書いたほうが例外安全の観点からは良いことも多くありますが)。むしろまともなRAII対応メンバを持っていればデストラクタを自分で定義していてもデメリットらしいデメリットは無い一方、コピーコンストラクタや代入演算子等は常にメンバの追加に対して一般的に保守が必要になるので厄介な存在と言えます。またshared_ptrはコピーができますがこれはshallow copyなので意図した挙動とは異なる場合も多いです。そこで登場するのが我らboost.optionalです。optionalならdeep copyですから直観的な挙動と同じになってくれます。インターフェイスもポインタライクでunique_ptrなどからの置き換えも容易です。しかしながら問題点もあります。それはmove非対応なことです。イマドキそれはないよね。ということでstd.optionalに期待がかかるわけですがどうやら当分先のことになりそうです。最後の希望はexperimental/optionalですがどの環境でも使用できるかは疑問なので積極的に使う気になれません。
そんなわけでstd.optionalが来るまでは下記の拙作pimplイディオムライブラリを使用しての解決をおすすめしてみます。
追記: boost 1.56.0よりoptionalがmoveをサポートしました。
標準ライブラリだけを使用するならvectorなどのコンテナを薄くラップして使う方法も考えられます。
ダメな例
struct Name { Type* member = nullptr; //生ポインタは論外! ~Name() { delete member; } void Prepare() { delete member; member = new Type{}; } void Use() { if (member) Type& r = *member; } }; void f() { Name a,b; a.Prepare(); b.Prepare(); a = b; //a.memberのポインタがリーク } //破棄時に二重deleteで未定義動作
#include <memory> struct Name { std::unique_ptr<Type> member; void Prepare() { member.reset(new Type{}); } void Use() { if (member) Type& r = *member; } }; void f() { Name a,b; a = b; //コピーできない }
#include <memory> struct Name { std::unique_ptr<Type> member; Name() = default; //間違いの元、保守性の低下 Name(Name const& rhs) : member(new Type(*rhs.member)) {} Name& operator = (Name const& rhs) { this->member.reset(new Type(*rhs.member)); } void Prepare() { member.reset(new Type{}); } void Use() { if (member) Type& r = *member; } };
解決策
#include <boost/optional.hpp> struct Name { boost::optional<Type> member; void Prepare() { member = Type{}; } void Use() { Type& r = member.value(); } };
#include <vector> struct Name { std::vector<Type> member; void Prepare() { member.assign(1, Type{}); } void Use() { Type& r = member.at(0); } };
不完全型を扱いたい場合
クラスの定義が循環して(木など再帰的な構造)いてメンバが不完全型であるためにコンパイルできないという状況はよく発生します。このような状況ではメンバにポインタ型(ポインタ自体は完全型)を持たせそれを経由させるのが回避策となりますが、上記と同じような問題が発生します。またこの場合には標準ライブラリのコンテナも回避策には使えません(将来的には不完全型が扱えることを保証する方向のようですが)。そこで下記の拙作pimplイディオムライブラリを使用しての解決をおすすめしてみます。注意点としてはデフォルトでオブジェクトが構築されるので定義が循環したクラスだとメモリを食い尽くしてしまいます。そこでpimpl_noinitと組み合わせて必要に応じてオブジェクトを生成してください。
PImplイディオムを実現したい場合
ヘッダ間の依存を減らすPImplイディオムとしてunique_ptrを使用した場合は上記の問題に加えてクラスのコンストラクタとデストラクタをソースファイル側に記述する必要性が生まれてしまいます。shared_ptrを使用した場合はデストラクタを記述する必要はありませんが挙動がshallow copyになってしまいます。またunique_ptr、shared_ptr共にdeep copyするにはソースファイル側にコピーコンストラクタや代入演算子を定義する必要が出てきてしまいます。またoptionalはpimplとは無縁の存在です。
そこでpimplライブラリを作ってみました。このライブラリの利点はdeep copyで値セマンティクスを実現し、move対応、必要なのはソースファイル側でコンストラクタを定義するか値を構築するだけです。もちろんヘッダ間の依存関係を減らすことができます。
#include "pimpl.hpp" class Type; //前方宣言のみ ヘッダのインクルードや定義はソースファイル側で struct Name { Name(); //コンストラクタ定義はソースファイル側で gununu::pimpl<Type> pmember; }; //値の構築を遅らせるならコンストラクタ定義不要。 //値の構築はソースファイル側で struct Name { gununu::pimpl<Type> pmember = gununu::pimpl_noinit; };
//ソースファイル側 #include "Name.hpp" //ここでTypeの定義をincludeしたり、直接定義する。 //複数のメンバはまとめて即席の構造体を定義するのもよい。 //namespaceの絡む前方宣言は面倒なので単独の型でも構造体でラップした方が楽 struct Type { Heavy a, b; }; Name::Name() = default; //値の構築はソースファイル側で行われる必要がある //Typeを実際に使用するときはソースファイル側の関数で void Name::SomeFunc() { pmember->a; pmember->b; }
PImplイディオムとして使用するつもりが無ければ前方宣言、コンストラクタ定義やソースファイル側での構築の必要はありません。
#include "pimpl.hpp" struct Name { gununu::pimpl<Type> pmember; //デフォルトで値が構築されるので構築したくない場合は //gununu::pimpl<Type> pmember = gununu::pimpl_noinit; };
現状optionalやスマートポインタとは違いデフォルトで値が構築されるのですが挙動を合わせたほうがいいのだろうか。
ライブラリ作成のコツ
普通にヘッダに関数を書くとinline化されて利用者側でも完全型が要求されpimplライブラリとして成り立たない。そこでinline化を回避する方法としてvirtual関数を使用している。