きままにブログ

プログラミングを主とした私のメモ帳です。寂しいのでコメントください笑

C4239TypeからType&への変換です

問題

#include <iostream>
#include <cassert>

using namespace std;

class C {
public:
	C() {
		cout << "create" << endl;
	}
	/*C(C&) {
		cout << "copy" << endl;
	}*/
};
C func() {
	C c;
	return c;
}

int main() {
	C c = func();

	cin.get();
	return 0;
}

においてコメントアウトすると警告なしにコンパイルが通るが、コメントアウトを外すとc = func()の部分でC4239の警告が出る。これはどうしてだろうか? 次のようにconstをつけてコピーコンストラクタを呼び出した場合はエラーが出ない。

class C {
public:
	C() {
		cout << "create" << endl;
	}
	C(const C&) {
		cout << "copy" << endl;
	}
};

なお、コピー/ムーブコンストラクタは、const Type&/const Type&&を引数として持つコンストラクタコンパイラによって自動生成される。ただし、コピー/ムーブコンストラクタは、第1引数にconst, volatileの有無によらないType&/Type&&を持ち、第2引数以降がないか全てデフォルト引数であるよるなものである。そのため、上記のように定義したC(C&)はコピーコンストラクタである。

考察

関数の戻り値が右辺値として扱われているのに対し、その右辺値を受け取るコンストラクタがないことが原因と考えられる。右辺値を受け取るムーブコンストラクタを付け加えるとコンパイルが通る。

#include <iostream>

using namespace std;

class C {
public:
	C() {
		cout << "create" << endl;
	}
	C(C&) {
		cout << "copy" << endl;
	}
	C(C&&) {
		cout << "move" << endl;
	}
};

C func() {
	C c; // create
	return c; // copy
}

int main() {
	C c(func()); // move(省略)
	cin.get();
	return 0;
}

しかし、出力結果は上記の通り、createしてからのcopyのみ。moveコンストラクタは実行されない。次のようにするとcreate->copy->moveとなるようだ。

int main() {
	C c(move(func())); // move(省略されなくなる)
	cin.get();
	return 0;
}

上記のテストはVisual Studio Express 2013でのDebugモードでの結果である。Releaseにするとcopyが実行されなくなる。これはDebugモードではRVO(Return Value Optimization)といって、一時オブジェクトの戻り値の最適化だけがされているのに対し、Releaseモードでは、NRVO(Named Return Value Optimization)といって同一のローカル変数までもが最適化されるためである。

これらの最適化がない場合は、func()の戻り値が素直にコンストラクタに渡されることになる。func()の戻り値自体は右辺値であるため、コンストラクタには渡せない。従って、最適化の有無によりコンパイルエラーに差異が出ないようにこのような警告を出すのだと考えられる。

結局、今回の問題はC(const C&)とコピーコンストラクタを定義せずに、C(C&)と定義したことによって、ムーブコンストラクタを実行しようとした際に生じたことに因る。戻り値云々は関係がなかった。以下が問題を示す最小限のコードである。

class C {
public:
	C() {
		cout << "create" << endl;
	}
	C(C&) {
		cout << "copy" << endl;
	}
	/* こちらならエラーにならない!!
	C(const C&) {
		cout << "copy" << endl;
	}
	*/
};

int main() {
	C c; // create
	C c2(move(c)); // ここでCからC&へ変換するエラー

	cin.get();
	return 0;
}

こうなるとconstの有無が焦点となる。ムーブコンストラクタは、ユーザー定義のコピーコンストラクタなどがない場合、暗黙的に定義されることになっている。また、明示的にも暗黙的にも定義されていない場合、即ち今回のようにコピーコンストラクタのみ定義している場合はムーブコンストラクタを呼び出す代わりにコピーコンストラクタを呼び出すことになっている。

従って、これはC(C&)をコピーコンストラクタと見なしていないと考えらる。