TadaoYamaokaの日記

山岡忠夫Homeで公開しているプログラムの開発ネタを中心に書いていきます。

C++でemplace_backを使う際の注意点

STLvectorのemplace_backを使うと要素の追加時にコンストラクタで初期化できる。

しかし、コンストラクタとデストラクタで副作用のある処理をしている場合、注意が必要だ。
以下のようなコードはうまく動作しない。

#include <iostream>
#include <vector>

using namespace std;

class A {
public:
	A(const size_t size_) : size(size_), a(new int[size_]) {
		cout << "create " << size << endl;
	}
	~A() {
		cout << "delete " << size << endl;
		delete[] a;
	}
private:
	size_t size;
	int* a;
};

int main()
{
	vector<A> vec;
	for (size_t i = 0; i < 2; i++) {
		vec.emplace_back(i + 1);
	}
	return 0;
}

createは2回呼ばれるが、deleteが3回以上呼ばれる。(3回目でメモリ例外になる。)

Visual C++ 2015の場合、2回目のemplace_backの際に、1つ目の要素のdeleteが呼ばれる。
処理系によって動作が異なる。

この動作はvectorが容量を拡張する際に、コピーコンストラクタが呼ばれることによる。

コピーコンストラクタを定義して、確認してみる。

class A {
public:
	A(const size_t size_) : size(size_), a(new int[size_]) {
		cout << "create " << size << endl;
	}
	A(const A& o) {
		cout << "copy " << size << endl;
		size = o.size;
		a = o.a;
	}
	~A() {
		cout << "delete " << size << endl;
		delete[] a;
	}
private:
	size_t size;
	int* a;
};

このコードを実行すると、

create 1
copy 14829735431805717965
delete 1
create 2
delete 1

のように表示される。3回目のdeleteでメモリ例外になる。

対策方法

reserveであらかじめ領域を確保することで、コピーコンストラクタが呼ばれることを避けることができる。

int main()
{
	vector<A> vec;
	vec.reserve(2);
	for (size_t i = 0; i < 2; i++) {
		vec.emplace_back(i + 1);
	}
	return 0;
}

このようにすれば、実行結果は、

create 1
create 2
delete 1
delete 2

となり、デストラクタが2回以上呼ばれることはなくなる。

参考:
C++ vector emplace_back calls copy constructor - Stack Overflow

2018/2/25 追記

平岡さんよりムーブコンストラクタを定義すればよいとコメントをいただきました。

vectorは、領域を拡張する際、メモリの再配置を行うため、要素のムーブを行っている。
このときムーブコンストラクタが定義されていないとコピーコンストラクタが呼ばれてしまう。
コピーした後、コピー元の要素のデストラクタが呼ばれる。
上記コード例のように、コピー先と、コピー元でポインタが同じnewしたオブジェクトを指している場合、コピー元のデストラクタで破棄してしまうと、コピー先で参照できなくなる。
このことは、「プログラミング言語C++ 第4版」にも書かれていた。

クラスを設計する際は、オブジェクトがコピーされる可能性とコピーの方法を必ず検討しなければならない。単純な具象型であれば、メンバ単位のコピーが正しいセマンティクスとなることが多い。しかし、Vectorのような高度な具象型では、メンバ単位のコピーは、正しいセマンティクスとはならない。また、抽象型では、メンバ単位のコピーは、まず、ありえない。

ビャーネ・ストラウストラップ. プログラミング言語C++ 第4版 (Kindle の位置No.3641-3644). . Kindle 版.

ムーブコンストラクタを定義したコードは以下のようになる。

class A {
public:
	A(const size_t size_) : size(size_), a(new int[size_]) {
		cout << "create " << size << endl;
	}
	A(A&& o) : size(o.size), a(o.a) {
		cout << "move " << size << endl;
		o.a = nullptr;
	}
	~A() {
		cout << "delete " << size << endl;
		delete[] a;
	}
private:
	size_t size;
	int* a;
};

このコードを実行すると、結果は、

create 1
move 1
delete 1
create 2
delete 1
delete 2

となり、正常に実行できる。

ポイントは、ムーブコンストラクタで、o.aにnullptrを代入している箇所だ。
nullptrを代入しておくことで、デストラクタでdelete[]を行ってもムーブ元が参照していたaが破棄されることがなくなる。

unique_ptrを使う方法

unique_ptrを使うことでnullptrの代入とdelete[]を自動的に行うことができる。
こちらが平岡さんに示していただいたコードだ。

class A {
public:
	A(const size_t size_) : size(size_), a(new int[size_]) {
		cout << "create " << size << endl;
	}
	A(A&& o) : size(o.size), a(move(o.a)) {
		cout << "move " << size << endl;
	}
	~A() {
		cout << "delete " << size << endl;
	}
private:
	size_t size;
	unique_ptr<int[]> a;
};

こちらの方が、よりC++的なコードだろう。