vectorとunique_ptr その3
vectorはコピーを使う、だからauto_ptrとおなじくunique_ptrは使えない。
以下のコードはコンパイルエラーが発生する。
vector<unique_ptr<C> > v; v.push_back( unique_ptr<C>( new C ) );
まれに、このコードをおすすめするHPが見られるが、コンパイルできるコンパイラーがあるのだろうか?
コンパイルできたとしても、今まで見てきたように、sortなどで副作用が出るので行うべきではない。
C++11対応のコンパイラーであれば、push_backの代わりにemplace_backが使えるかもしれないが、セットは可能でも、やはりsortで問題が発生する。
対処方法はやはりshared_ptrに置き換えればよい。
vector< shared_ptr<C> > v; v.push_back( shared_ptr<C>( new C ) );
shared_ptrのコストを避けようとするのであれば、削除に備える必要がある。
typedef std::vector<unique_ptr<C> > Vip; typedef std::vector<unique_ptr<C> >::iterator Vip_itr; typedef std::vector<unique_ptr<C> >::const_iterator Vip_citr; class D { void _clear( Vip_itr iSt, Vip_itr iEd ) { for( ; iSt < iEd; ++iSt ) { if( *iSt ) delete *iSt; } } void _copy( Vip &dst, Vip_citr iSt, Vip_citr iEd ) { for( ; iSt < iEd; ++iSt ) { dst.push_back( new int(**iSt) ); } } public: D() {}; D( const C& lhs ) { _copy( v, lhs.v.begin(), lhs.v.end() ); } Vip v; void resize( unsigned int size ) { if(size<v.size()) _clear( v.begin()+size-1, v.end() ); } Vip_itr erase( Vip_itr itr ) { if( ( itr != v.end() )&&( *itr ) ) delete *itr; return v.erase( itr ); } void clear() { _clear( v.begin(), v.end() ); } };
D v;
v.v.push_back(unique_ptr<C>(new C));
これでも抜けがあるかもしれない。実際、重複したポインター参照などを考慮していない。vectorをそのまま使えないことも問題があるだろう。
文芸的にはよいかもしれないが、実践的ではないと思う。
vectorとunique_ptr その2
前回はコピーすべきところでムーブすべきではないことがわかった。
コードはshared_ptrを用いることで、すべてがうまくいくようになる。
class C { public: shared_ptr<int> d; C() : d(new int(0)) {} C( const int n ) : d(new int(n)) {} // コピーコンストラクタ、オペレータ=は自動生成でok ~C() {} }; bool operator<(const C& lhs, const C& rhs ) { return lhs.d.get() < rhs.d.get(); } 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() ); // OK }
ただし、shared_ptrは生のポインターやunique_ptrを用いる場合と比べてリソースやスピードなどのコストがかかる。
ポインターをムーブさせることがだめなのであるから、コピーはコピーということに立ち返れば、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(new int(*lhs.d.get())) {}// コピーコンストラクタ const C& operator=( const C& lhs ) // 代入 { *d.get() = *lhs.d.get(); return *this; } ~C() {} }; bool operator<(const C& lhs, const C& rhs ) { return *lhs.d.get() < *rhs.d.get(); } 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() ); // OK }
上記コードでは、コピーでポインターをムーブさせるのではなく、ポインターの先をコピーさせるように変更している。
どちらがよいのだろうか?
shared_ptr版は、ポインターの先を共有するので、ポインターのコピーとしてはサイズが大きくなりかつ低速となる。しかしオブジェクトのコピーを行わない。
unique_ptr版は、ポインターの先を共有しない。このためポインターのコピーとは別にオブジェクトの生成コストがかかる。通常オブジェクトの生成はポインターのコピーよりずっと遅い。
ポインターを使いたくなるシーンを考えると、class Cの実装はshared_ptr版の方がメモリーサイズも速度も速くなる可能性が高いと思われる。また、書くべきコードも少なく見通しもよいだろう。
unique_ptr版の方がよい場合は、vectorにすべて異なるオブジェクトを格納し、かつ静的なコンテナ(変更しない読み出し専用)として用いる場合だけではないだろうか。
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のまねごとなのだ。
コピーをすべき箇所でムーブはできないと考えるべきなのだ。