std::auto_ptr_ref と Colvin-Gibbons トリック
std::auto_ptr
は、C++ での RAII を実現するにあたって欠かせない重要なクラス((厳密に言えば std::auto_ptr
は「構造体テンプレート」だが、面倒なので「クラス」と呼ぶ。))だ。だが、その根底にある move semantics という考え方を正しく理解していないと深刻なバグの原因にもなりかねない、危険なクラスでもある。std::auto_ptr
の正しい使い方を学ぶ方法はいろいろとあるだろうが、ここでは、実際に実装することで、その背景にある考え方や C++ の言語仕様も併せて見てみることにする。
RAII
まずは、RAII の基本を復習しておこう:
#include <iostream> #include <ostream> class my_data { public: my_data() { std::cout << "created!" << std::endl; } ~my_data() { std::cout << "destroyed!" << std::endl; } }; class my_ptr { my_data* m_data; // コピー不能にする my_ptr(const my_ptr&); my_ptr& operator = (const my_ptr&); public: explicit my_ptr(my_data* const data = 0) throw() : m_data(data) { } ~my_ptr() throw() { delete m_data; } }; int main() { my_ptr raii( new my_data() ); return 0; }
my_ptr
のデストラクタが m_data
を delete
するため、フリーストアに生成した my_data
をリークすることがない。
move semantics
上記のようなシンプルな RAII クラス*1の問題点は、リソースの寿命が RAII クラスのスコープに限定されてしまうことだ。そこで、以下のような“コピー”コンストラクタと“コピー”代入演算子を導入する:
my_ptr(my_ptr& another) throw() : m_data(another.m_data) { another.m_data = 0; } my_ptr& operator = (my_ptr& rhs) throw() { my_data* const tmp( rhs.m_data ); // ここで一次変数に待避しているのは、自己代入への備え。 rhs.m_data = 0; m_data = tmp; return *this; }
代入演算子はコピーコンストラクタと swap
関数で実装するべきだが、std::auto_ptr
には swap
関数はないのでそれに倣った。
「コピー」と言いつつ全然コピーじゃないのがポイントで、“コピー”コンストラクタや“コピー”代入演算子は、m_data = 0
のようにコピー元を変更してしまう。つまり、リソースの所有権を“強奪”するわけだ。もしくは、オブジェクトの実質的な中身がコピー元からコピー先に移動したという見方もできる。これがいわゆる「move semantics」で、「コピーコンストラクタ」に対して「ムーブコンストラクタ」と呼んだりもする。
兎にも角にも、これにより、リソースを生成するファクトリ関数が書けるようになる:
void create_data(my_ptr& receiptor) { my_ptr data( new my_data() ); receiptor = data; } int main() { my_ptr data; create_data(data); return 0; }
ちなみに、RAII なオブジェクトの生成は独立したステートメントで行うべきなので、create_data
関数で receiptor = my_ptr(new my_data());
とするのは良くない*2。
非 const
参照な引数には右辺値を渡せない
上記の create_data
関数では引数に取った my_ptr
の参照にコピー (ムーブ) することで結果を返しているが、これは明らかに不自然だ。なぜ、このように返却値で返さないのか:
my_ptr new_data() { my_ptr data( new my_data() ); return data; } int main() { my_ptr raii( new_data() ); return 0; }
実は、このコードはコンパイルできない。たとえば、g++ 4.0.2 では以下のようなエラーになる:
- エラーメッセージ
-
In function ‘int main()’: error: no matching function for call to ‘my_ptr::my_ptr(my_ptr)’ note: candidates are: my_ptr::my_ptr(my_ptr&) note: my_ptr::my_ptr(my_data*)
- 意訳
-
my_ptr::my_ptr(my_ptr)
なんてコンストラクタはねーよ。my_ptr::my_ptr(my_ptr&)
ならあるけど、それのつもりですか?
const
参照な引数には右辺値を渡せないと C++ 標準*3で決められているのだ:- 13.3.3.1.4 Reference binding
-
A standard conversion sequence cannot be formed if it requires binding a reference to non-const to an rvalue (except when binding an implicit object parameter; see the special rules for that case in 13.3.1). [Note: this means, for example, that a candidate function cannot be a viable function if it has a non-const reference parameter (other than the implicit object parameter) and the corresponding argument is a temporary or would require one to be created to initialize the reference (see 8.5.3). ]
一時的な release()
上記の問題への対処として、my_ptr
をただの my_data*
に戻すメンバ関数 release()
を用意してこのようにする方法がある:
int main() { my_ptr raii( new_data().release() ); return 0; }
ただのポインタなら普通にコピーできるので、参照で受け取る必要はない。release()
による所有者不在の状況は一時的なものなので、リソースリークの危険性もない。ちなみに、release
関数の実装はこんな感じだ:
my_data* release() throw() { my_data* const released( m_data ); m_data = 0; return released; }
Colvin-Gibbons トリック
一時的な release()
は一応問題を解決するが、どうにも美しくない。こんなことはしたくないし、事実、std::auto_ptr
は普通に返却値に使える。一体どうやっているのか? もちろん、“とりあえず const
参照でもらっておいて const_cast
”なんていう愚行は犯していない*4。
ここで登場するのが、Colvin-Gibbons トリックと呼ばれる奥義だ。結論から言うと、こうする:
class my_ptr { //【追加】転送オブジェクト struct my_ptr_ref { my_data* m_data; }; my_data* m_data; public: explicit my_ptr(my_data* const data = 0) throw() : m_data(data) { } my_ptr(my_ptr& another) throw() : m_data(another.release()) { } //【追加】転送オブジェクトからのコピー my_ptr(my_ptr_ref colvinGibbonsTrick) : m_data(colvinGibbonsTrick.m_data) { } ~my_ptr() throw() { delete m_data; } my_ptr& operator = (my_ptr& rhs) throw() { m_data = rhs.release(); return *this; } //【追加】転送オブジェクトからの代入 my_ptr& operator = (my_ptr_ref rhs) throw() { m_data = rhs.m_data; return *this; } //【追加】転送オブジェクトへのムーブ operator my_ptr_ref() { my_ptr_ref colvinGibbonsTrick; colvinGibbonsTrick.m_data = release(); return colvinGibbonsTrick; } my_data* release() throw() { my_data* const released( m_data ); m_data = 0; return released; } };
わけの分からない my_ptr_ref
とかいう代物が唐突に出現したが、とにかく、これで問題は解決し、このコードもコンパイルできるようになる:
int main() { my_ptr raii( new_data() ); return 0; }
Colvin-Gibbons トリックの本質は、返却値の一次オブジェクトに対する release()
の呼び出しを暗黙に行わせることだ。それには release
を operator my_data*
に変えてしまえばいいわけだが、そうすると、意図しないところで勝手に release()
されるおそれがあってあまりにも危険だ。そこで、my_ptr_ref
という型を用意する。これは release()
されたポインタと同じ役割を果たすが、コンストラクタ my_ptr::my_ptr(my_ptr_ref)
に受け取られる以外に使える場所がない。これにより、所有権の受け入れ先がある場合にのみ release()
を自動的に呼ぶ、という動作が実現できるわけだ。
仕上げ: 汎用のスマートポインタにする
以上が Colvin-Gibbons トリックによる move semantics の実現だ。最後に、テンプレート化して my_data
型への依存性を排除するとともに若干のメンバ関数を追加すれば、汎用のスマートポインタとしての my_ptr
, すなわち std::auto_ptr
が完成する:
//【変更】my_ptr_ref は my_ptr の外に出して独立させる。 template <typename T> struct my_ptr_ref { T* m_data; }; template <typename T> class my_ptr { T* m_data; public: //【追加】std::auto_ptr はこの typedef を提供している typedef T element_type; explicit my_ptr(T* const data = 0) throw() : m_data(data) { } my_ptr(my_ptr& another) throw() : m_data(another.release()) { } //【追加】ポインタとしての暗黙のキャストを可能にするコンストラクタテンプレート template <typename U> my_ptr(my_ptr<U>& another) throw() : m_data(another.release()) { } my_ptr(my_ptr_ref<T> colvinGibbonsTrick) : m_data(colvinGibbonsTrick.m_data) { } ~my_ptr() throw() { delete m_data; } my_ptr& operator = (my_ptr& rhs) throw() { m_data = rhs.release(); return *this; } //【追加】ポインタとしての暗黙のキャストを可能にする代入演算子テンプレート template <typename U> my_ptr& operator = (my_ptr<U>& rhs) throw() { m_data = rhs.release(); return *this; } my_ptr& operator = (my_ptr_ref<T> rhs) throw() { m_data = rhs.m_data; return *this; } //【追加】他の型の my_ptr への変換演算子 // よく分からないが、変換コンストラクタではダメな場合もあるのだろう。 template <typename U> operator my_ptr<U>() throw() { my_ptr<U> converted( release() ); return converted; } //【変更】ポインタとしての暗黙のキャストを可能にするために、 // my_ptr_ref への変換演算子もテンプレート化する template <typename U> operator my_ptr_ref<U>() { my_ptr_ref<U> colvinGibbonsTrick; colvinGibbonsTrick.m_data = release(); return colvinGibbonsTrick; } T* release() throw() { T* const released( m_data ); m_data = 0; return released; } //【追加】std::auto_ptr には get がある T* get() const throw() { return m_data; } //【追加】std::auto_ptr には reset がある void reset(T* const data = 0) throw() { if (data == m_data) { return; } delete m_data; m_data = data; } //【追加】スマートポインタとしての * 演算子 T& operator * () const throw() { return *m_data; } //【追加】スマートポインタとしての -> 演算子 T& operator -> () const throw() { return m_data; } };
my_ptr_ref
を my_ptr
の外に出すのは、my_ptr<T>
と my_ptr<U>
は全く関係のない別のクラスなので互いの private
メンバには触れられなくなるため。だが、これにより、こういうリソースリークのおそれが現れてしまっている:
int main() { my_ptr<int> raii( new int() ); static_cast< my_ptr_ref<int> >(raii); // このキャストは raii.release() と同義!! return 0; }
まあ、std::auto_ptr_ref
は処理系依存のブラックボックスで言及すること自体が禁止なので、大丈夫といえば大丈夫だろう。できれば、std::_private
みたいないかにもヤバそうな名前空間にでも入れておいてくれればなおよかったのだが。