C++のRAIIは素晴らしい。
ご存知の通りRAIIはコンストラクタとデストラクタの対称性を利用した資源の解放忘れのリスクを減らすテクニックである。
例えば、以下の様な安易な資源の作成には、資源の解放忘れによるメモリリークのリスクが存在する。
int main() {
auto p = new int{};
if (p && *p) {
std::cout << *p;
delete p;
}
// *p==0の時メモリリークする
}
この様なリスクを減らす為にRAIIを利用する。
コンストラクタに作成した資源を渡し、デストラクタで資源を解放する。
int main() {
struct RAII {
RAII(int* p) : p_(p) {}
RAII(RAII const&) = delete;
RAII& operator =(RAII const&) = delete;
~RAII() { if(p_) { delete p_; } }
int& operator *() { return *p_; }
explicit operator bool() const { return p_; }
private:
int* p_;
} p{new int{}};
if (p && *p) {
std::cout << *p;
}
// pの資源はここで解放される
}
RAIIは便利で安全だが、その都度コンストラクタとデストラクタを定義するのは面倒である。
そこで標準機能のstd::unique_ptrがある。
std::unique_ptrを利用する事で、上記のコードが以下のように短縮できる。また、任意の位置で資源を開放できる。
int main() {
std::unique_ptr<int> p(new int{});
if (p && *p) {
std::cout << *p;
p.reset();
}
// *p==0であれば、pの資源はここで解放される
}
std::unique_ptrには資源を開放する時の手続きを変更する機能(デリーターの指定)もある。
テンプレート実引数の第2引数に第1引数のポインタを受け取る関数型を渡せばよい。
例えば、資源を解放する直前に内容を表示したい場合は以下の様にデリーターを指定する。
int main() {
auto deleter = [](int* p){ std::cout <<*p; delete p; };
std::unique_ptr<int, decltype(deleter)> p(new int{}, deleter);
}
実にシンプルで美しい機能である。しかし、資源が常にポインタであるとは限らない。
例えば、モジュールの初期化と解放が対の関数になっている場合である。実際に、この様な関数を持つモジュールは多数存在する。
この時、やはり解放の関数(例ではmylib::term)を忘れてしまうリスクが存在する。
int main() {
auto good = mylib::init();
if (good && mylib::setup()) {
mylib::update();
mylib::term();
}
// mylib::setup()が失敗すると解放されない
}
モジュールはポインタではない為、std::unique_ptrが利用できない。とは言え、前述のRAII型を定義するのは面倒である。
ここで、ポインタではない資源に対するRAIIに於いてstd::unique_ptrを利用するテクニックを紹介する。
std::unique_ptrは保持するポインタがnullptrである場合はデリーターを呼び出さない。
そこで、モジュールの初期化が成功した時にnullptrでないポインタを設定する。失敗した場合はnullptrを設定する。
これにより、初期化が成功した時にだけ解放関数が呼び出される。そして成功した時に設定するポインタはnullptrでなければなんでも良い。例えば、以下の様にvoidポインタとしてstd::unique_ptr自身のアドレスを渡しても良い。
未初期化のインスタンスのポインタを渡す事になる。しかし、このポインタが真偽テスト以外に利用される事はない。大丈夫だ、問題ない。
int main() {
auto mylib_term = [](void*){ mylib::term(); };
std::unique_ptr<void, decltype(mylib_term)> mylib_raii(mylib::init() ? &mylib_raii : nullptr, mylib_term);
if (mylib_raii && mylib::setup()) {
mylib::update();
mylib_raii.reset();
}
// mylib::setup()に失敗しても解放される
}
どうしても心配な場合は、デリーターのポインタを渡しても良い。
この時constなstd::unique_ptrに指定できる為、より堅牢になる場合がある。
int main() {
auto mylib_term = [](void*){ mylib::term(); };
std::unique_ptr<void, decltype(mylib_term)> const mylib_raii(mylib::init() ? &mylib_term : nullptr, mylib_term);
if (mylib_raii && mylib::setup()) {
mylib::update();
}
}
以上、怠惰なRAIIでした。