C++のRAIIは素晴らしい。デストラクタを明示的に呼べる事で実現する、このテクニックはもはやC++クラス設計の基本である。
さて、前回はstd::unique_ptrを利用してポインタを持たない資源の自動的な解放の実験を行った。今回はクラスのメンバ変数が資源である場合のRAIIについて検討する。
当然の話だが、クラスのメンバ変数は資源となる事がある。例えばヒープ領域にインスタンスを作成する。
struct X {
int* p = new int{42};
};
int main() {
X x;
std::cout << *x.p; // 42
// x.pはメモリリークする
}
ここで、Xのインスタンス(変数x)が作成されると同時に、Xのメンバ変数pがヒープ領域に作成したインスタンスを指す。しかし、変数xが破棄されてもヒープ領域に作成したインスタンスは破棄されないし、メモリも解放されない。よって、メンバ変数pは明示的に解放しなければならない。この時、Xの資源はXが破棄されると同時に破棄されるべきである。これを実現する為にデストラクタが存在する。
# include <iostream>
struct X {
~X() { delete p; }
int* p = new int{42};
};
int main() {
X x;
std::cout << *x.p;
// x.pはXのデストラクタによって解放される
}
ところが、何者かにpが変更されたり、Xのインスタンスがコピーされたりすると期待通りに動作しない。
int main() {
X x;
x.p = new int {1};
auto y = x;
}
頑張ってカプセル化するのは面倒なので、ここではstd::unique_ptrを利用する。
# include <iostream>
# include <memory>
struct X {
std::unique_ptr<int> p { new int{42} };
};
int main() {
X x;
std::cout << *x.p;
}
実にシンプルで美しい。しかし、ここでstd::unique_ptrのデリーターを変更する場合は注意が必要である。先ず、デリーターをラムダ式にしたい時に、クラスの定義中はラムダ式を変数で束縛できない。よってラムダ式の型を得る事もできない。
struct X {
static auto deleter = [](int* p){ delete p; }; // エラー
std::unique_ptr<int, decltype(deleter)> p { new int{42}, deleter };
};
ラムダ式をクラス定義の外で予め束縛しておけば使用できるが、手続きがクラス外に露出しており、あまり美しくない。
auto deleter = [](int* p){ delete p; };
struct X {
std::unique_ptr<int, decltype(deleter)> p { new int{42}, deleter };
};
幸運な事に、キャプチャのないラムダ式は関数ポインタに変換可能である。嬉々としてデリーターの型を関数ポインタにして、コンストラクタにラムダ式を渡す。
struct X {
std::unique_ptr<int, void(*)(int*)> p { new int{42}, [](int* p) { delete p; } };
};
このコードは期待通りに動作するし、美しい。しかし、デリーターをラムダ式の型にした場合に比べてstd::unique_ptrのサイズが増している。これは、std::is_empty<deleter>::valueが真の時に、内部のポインタホルダーがデリーターの機能を継承する事でデリーターの実態が不要になるからである。一方、関数ポインタの場合は関数ポインタをメンバとして保持しなくてはならない。よって、その分だけサイズが増す。
auto deleter = [](int* p){ delete p; };
struct X {
std::unique_ptr<int, void(*)(int*)> p { new int{42}, deleter };
std::unique_ptr<int, decltype(deleter)> q { new int{42}, deleter };
static_assert(sizeof(p) > sizeof(q), ""); // OK
};
という訳で、クラスのメンバ変数にstd::unique_ptrを置く場合はラムダ式ではなく、デリータークラスを定義する方が良い。デリータークラスが空であればstd::unique_ptrのサイズはラムダ式の時と同じである。
# include <memory>
auto deleter = [](int* p){ delete p; };
struct X {
std::unique_ptr<int, void(*)(int*)> p { new int{42}, deleter };
std::unique_ptr<int, decltype(deleter)> q { new int{42}, deleter };
static_assert(sizeof(p) > sizeof(q), ""); // OK
struct Deleter {
void operator ()(int* p) const { delete p; }
};
std::unique_ptr<int, Deleter> r { new int{42} };
static_assert(sizeof(q) == sizeof(r), ""); // OK
};
ところで、資源がポインタでなかった場合はどうするべきだろうか。例えばモジュールの初期化と解放を考えたとして、前回と同様にstd::uniuque_ptrを利用しても、結局デリータークラスを定義するのであれば、デリータークラスのコンストラクタとデストラクタを利用した方が早いし、サイズも小さくなる。例えば以下のメンバ変数yの様に書いた方が良い。
struct X {
struct Y {
~Y() { good && []{mylib::term(); return true; }(); }
explicit operator bool() const { return good; }
bool good = mylib::init();
} const y;
};
int main() {
X x;
if (x.y && mylib::setup()) {
mylib::update();
}
}
また、モジュールが他のモジュールに依存していたり、とある資源を作成するのに他の資源を要求される事が屡々ある。この様な場合は、クラスのメンバ変数が初期化の順になるようにする。アクセス指定が同じメンバ変数間では、先に定義した変数が先に初期化されるからである。
struct X {
struct Y {
~Y() { good && []{mylib::term(); return true; }(); }
explicit operator bool() const { return good; }
bool good = mylib::init();
} const y;
struct Z {
Z(Y const& y) : good(y && mylib::init()) {};
~Z() { good && []{mylib::term(); return true; }(); }
explicit operator bool() const { return good; }
bool good;
} const z{y};
};
余談だが、メンバ変数の初期化にthisポインタを渡す事ができる。thisポインタから前方のメンバ変数を参照すれば依存関係が多い場合に楽ができる可能性がある。しかし渡してはいけない。もしも未初期化のメンバ変数を参照した時はアウツ…それをやったら...。
struct X {
struct Y {
~Y() { good && []{mylib::term(); return true; }(); }
explicit operator bool() const { return good; }
bool good = mylib::init();
} const y;
struct Z {
Z(X* self) : good(self->y && mylib::init()) {};
~Z() { good && []{mylib::term(); return true; }(); }
explicit operator bool() const { return good; }
bool good;
} const z{this}; // アウツ...
};
以上、怠惰なRAIIでした。