vectorとunique_ptr その1
vectorとunique_ptrの相性は悪い。
vectorはコピーが伴うものだし、unique_ptrはコピーコンストラクタを持たない。
以下のようなコードを考えてみる。
class C { public: int d; C() : d(0) {} C( const int n ) : d(n) {} ~C() {} }; void test_code() { vector<C> v; C a(3); v.push_back(a); }
class C の内部オブジェクトはintであるが、もっと複雑な構造体である場合もある。
いくつかの理由でオブジェクトをポインターで保持したいとしよう。
class C { public: int* d; C() : d(new int(0)) {} C( const int n ) : d(new int(n)) {} C( const C& lhs ) // コピーコンストラクタ { d = const_cast<C&>(lhs).d; } const C& operator=( const C& lhs ) // 代入 { d = const_cast<C&>(lhs).d; return *this; } ~C() { if( d ) delete d; } // ポインターを共有する場合、確実にアクセス違反を起こす }; void test_code() { vector<C> v; C a(3); v.push_back(a); }
ただし、このコードでは、オブジェクトの廃棄ごとにdeleteが発行されるので、delete後のポインターでアクセスバイオレーションが発生する。
発生させないように、コピーの際にはコピー元のアドレスをNULLにすることを思いつく。
class C { public: int* d; C() : d(new int(0)) {} C( const int n ) : d(new int(n)) {} C( const C& lhs ) // コピーコンストラクタ { d = const_cast<C&>(lhs).d; const_cast<C&>(lhs).d = NULL; // コピー元のアドレスをNULLに } const C& operator=( const C& lhs ) // 代入 { d = const_cast<C&>(lhs).d; const_cast<C&>(lhs).d = NULL; // 代入元のアドレスをNULLに return *this; } ~C() { if( d ) delete d; } }; void test_code() { vector<C> v; C a(3); v.push_back(a); }
const_castがあって美しくない。unique_ptrを使ってみよう。
class C { public: unique_ptr<int> d; C() : d(new int(0)) {} C( const int n ) : d(new int(n)) {} C( const C& lhs ) // コピーコンストラクタ { d = move(const_cast<C&>(lhs).d); } const C& operator=( const C& lhs ) // 代入 { d = move(const_cast<C&>(lhs).d); return *this; } ~C() {} }; void test_code() { vector<C> v; C a(3); v.push_back(a); }
いまだにconst_castを用いていて美しくはないが同じ機能を実現できdeleteもなくなった。
しかしこのコードは、クィックソートでアクセスバイオレーションが発生する。
class C { public: unique_ptr<int> d; C() : d(new int(0)) {} C( const int n ) : d(new int(n)) {} C( const C& lhs ) // コピーコンストラクタ { d = move(const_cast<C&>(lhs).d); } const C& operator=( const C& lhs ) // 代入 { d = move(const_cast<C&>(lhs).d); return *this; } ~C() {} }; bool operator<(const C& lhs, const C& rhs ) { return *lhs.d < *rhs.d; } void test_code() { vector<C> v; C a(3), b(2), c(1); v.push_back(a); v.push_back(b); v.push_back(c); sort( v.begin(), v.end() ); // アクセスバイオレーション }
クィックソートは、内部でCをコピーして一時オブジェクトとして用いる。
func() { ・・・// 処理 C p = v[i]; // v[i].dはNULLがセットされる ・・・// 処理 } // pの削除に伴い、p.dもdeleteされる
このとき、v[i].dはコピーによりNULLとなり、p.dにポインターが入る。pとp.dのポインターはfunc()を抜けた瞬間削除される。
リソースリークは起きないが、v[i].dを次にアクセスしたときにアクセスバイオレーションが発生する。
このような現象は別にsortに限らず発生する可能性がある。プログラマーからすれば理解しにくいバグの温床となるためにauto_ptrは葬り去られた。
上のコードはポインター版もunique_ptr版もauto_ptrのまねごとなのだ。
コピーをすべき箇所でムーブはできないと考えるべきなのだ。