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