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のまねごとなのだ。
コピーをすべき箇所でムーブはできないと考えるべきなのだ。