読者です 読者をやめる 読者になる 読者になる

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