C++ 11 コピーと move と戻り値の関係

はじめに

C++ 11 になり、オブジェクトの move ができるようになったことで関数の戻り値の型にクラスを指定しても問題ない程度の負荷で済む場合も多くなりましたが、クラスの宣言の仕方によってはその恩恵を得られない場合もあります
ここでは関数の戻り値とクラスのインスタンスのコピーと move の関係について簡単に説明したいと思います

ここで説明する内容について標準では次の辺りに書いてあるようです

12 Special member functions
12.8 Copying and moving class objects

暗黙にコピーも move もできるクラス

暗黙にコピーも move もできるクラスというのは、コピーコンストラクタにも move コンストラクタにも explicit が指定されておらず = delete; されていたり private だったりしないクラスのことです
次のような普通のクラスのことです

戻り値の型が暗黙に move できるクラスである場合にはコピーではなく move されるので効率的です
C++ 11 ではクラスのインスタンスを戻り値で返しても問題ない程度の負荷で済む場合も多くなりました

// 暗黙にコピーも move もできる
class cfoo {
	std::string	m_a;
	std::string	m_b;
	std::string	m_c;
public:
	const char*	a() const { return	m_a.c_str(); }
	const char*	b() const { return	m_b.c_str(); }
	const char*	c() const { return	m_c.c_str(); }
	
	cfoo():m_a(),m_b(),m_c(){
		debug_printf( "cfoo::cfoo()\n" );
	}
	cfoo( const char*	a_, const char*	b_, const char*	c_ ):m_a(a_),m_b(b_),m_c(c_){
		debug_printf( "cfoo::cfoo( \"%s\", \"%s\", \"%s\" )\n", a(), b(), c());
	}
	cfoo( cfoo&&	rhs ):
	m_a(std::move(rhs.m_a)),
	m_b(std::move(rhs.m_b)),
	m_c(std::move(rhs.m_c))
	{
		debug_printf( "cfoo::cfoo( cfoo&& )\n" );
	}
	cfoo&	operator=( cfoo&&	rhs ){
		debug_printf( "cfoo::operator=( cfoo&& )\n" );
		m_a = std::move(rhs.m_a);
		m_b = std::move(rhs.m_b);
		m_c = std::move(rhs.m_c);
		return	*this;
	}
	cfoo( const cfoo&	rhs ):
	m_a(rhs.m_a),
	m_b(rhs.m_b),
	m_c(rhs.m_c)
	{
		debug_printf( "cfoo::cfoo( const cfoo& )\n" );
	}
	cfoo&	operator=( const cfoo&	rhs ){
		debug_printf( "cfoo::operator=( const cfoo& )\n" );
		if( this != &rhs ){
			m_a = rhs.m_a;
			m_b = rhs.m_b;
			m_c = rhs.m_c;
		}
		return	*this;
	}
	~cfoo(){
		debug_printf( "cfoo::~cfoo()\n" );
	}
};
inline cfoo	make_foo( const char*	a, const char*	b, const char*	c ){
	cfoo	foo( a, b, c );
	return	foo;	// move できるのであれば move される
}

出力例

inline void	print_foo( const cfoo&	foo ){
	debug_printf( "foo: a = \"%s\", b = \"%s\", c = \"%s\"\n", foo.a(), foo.b(), foo.c());
}
int	main(){
	print_foo( make_foo( "1", "2", "3" ));
	return	0;
}
// ローカル変数を作成
cfoo::cfoo( "1", "2", "3" )
// 戻り値へ move
cfoo::cfoo( cfoo&& )
// ローカル変数を破棄
cfoo::~cfoo()
// 戻り値を使用
foo: a = "1", b = "2", c = "3"
// 戻り値を破棄
cfoo::~cfoo()

暗黙にコピーはできるが move はできないクラス

暗黙にコピーはできるが move はできないクラスというのは、move コンストラクタに explicit が指定されていたり、= delete; されていたり private だったりするクラスのことです

戻り値の型が暗黙に move できないクラスである場合には戻り値はコピーされるので効率的ではありません
標準にも書いてある通り、このような明らかに不要なコピーは最適化によって除去されることもあるかもしれませんが、基本的には move できないクラスを戻り値にすべきではありません

リファレンスを返すのではダメなのか
戻り値ではなく出力引数ではダメなのか
move できるようにしてはダメなのか
そうしたところを検討してみましょう

// 暗黙にコピーはできるが move は明示的な指定が必要
class cbar {
	std::string	m_a;
	std::string	m_b;
	std::string	m_c;
public:
	const char*	a() const { return	m_a.c_str(); }
	const char*	b() const { return	m_b.c_str(); }
	const char*	c() const { return	m_c.c_str(); }
	
	cbar():m_a(),m_b(),m_c(){
		debug_printf( "cbar::cbar()\n" );
	}
	cbar( const char*	a_, const char*	b_, const char*	c_ ):m_a(a_),m_b(b_),m_c(c_){
		debug_printf( "cbar::cbar( \"%s\", \"%s\", \"%s\" )\n", a(), b(), c());
	}
	explicit cbar( cbar&&	rhs ):
	m_a(std::move(rhs.m_a)),
	m_b(std::move(rhs.m_b)),
	m_c(std::move(rhs.m_c))
	{
		debug_printf( "cbar::cbar( cbar&& )\n" );
	}
	cbar&	operator=( cbar&&	rhs ){
		debug_printf( "cbar::operator=( cbar&& )\n" );
		m_a = std::move(rhs.m_a);
		m_b = std::move(rhs.m_b);
		m_c = std::move(rhs.m_c);
		return	*this;
	}
	cbar( const cbar&	rhs ):
	m_a(rhs.m_a),
	m_b(rhs.m_b),
	m_c(rhs.m_c)
	{
		debug_printf( "cbar::cbar( const cbar& )\n" );
	}
	cbar&	operator=( const cbar&	rhs ){
		debug_printf( "cbar::operator=( const cbar& )\n" );
		if( this != &rhs ){
			m_a = rhs.m_a;
			m_b = rhs.m_b;
			m_c = rhs.m_c;
		}
		return	*this;
	}
	~cbar(){
		debug_printf( "cbar::~cbar()\n" );
	}
};
inline cbar	make_bar( const char*	a, const char*	b, const char*	c ){
	cbar	bar( a, b, c );
	return	bar;	// move できないのであればコピーされる
}

出力例

inline void	print_bar( const cbar&	bar ){
	debug_printf( "bar: a = \"%s\", b = \"%s\", c = \"%s\"\n", bar.a(), bar.b(), bar.c());
}
int	main(){
	print_bar( make_bar( "1", "2", "3" ));
	return	0;
}
// ローカル変数を作成
cbar::cbar( "1", "2", "3" )
// 戻り値へコピー
cbar::cbar( const cbar& )
// ローカル変数を破棄
cbar::~cbar()
// 戻り値を使用
bar: a = "1", b = "2", c = "3"
// 戻り値を破棄
cbar::~cbar()

暗黙に move はできるが、コピーはできないクラス

暗黙に move はできるが、コピーはできないクラスというのは、コピーコンストラクタに explicit が指定されていたり、= delete; されていたり、private だったりするクラスのことです

move できるのであれば戻り値には move が使われるので、この場合はコピーできないかどうかということは関係ありません

// 暗黙に move はできるが、コピーは明示的な指定が必要
class cbaz {
	std::string	m_a;
	std::string	m_b;
	std::string	m_c;
public:
	const char*	a() const { return	m_a.c_str(); }
	const char*	b() const { return	m_b.c_str(); }
	const char*	c() const { return	m_c.c_str(); }
	
	cbaz():m_a(),m_b(),m_c(){
		debug_printf( "cbaz::cbaz()\n" );
	}
	cbaz( const char*	a_, const char*	b_, const char*	c_ ):m_a(a_),m_b(b_),m_c(c_){
		debug_printf( "cbaz::cbaz( \"%s\", \"%s\", \"%s\" )\n", a(), b(), c());
	}
	cbaz( cbaz&&	rhs ):
	m_a(std::move(rhs.m_a)),
	m_b(std::move(rhs.m_b)),
	m_c(std::move(rhs.m_c))
	{
		debug_printf( "cbaz::cbaz( cbaz&& )\n" );
	}
	cbaz&	operator=( cbaz&&	rhs ){
		debug_printf( "cbaz::operator=( cbaz&& )\n" );
		m_a = std::move(rhs.m_a);
		m_b = std::move(rhs.m_b);
		m_c = std::move(rhs.m_c);
		return	*this;
	}
	explicit cbaz( const cbaz&	rhs ):
	m_a(rhs.m_a),
	m_b(rhs.m_b),
	m_c(rhs.m_c)
	{
		debug_printf( "cbaz::cbaz( const cbaz& )\n" );
	}
	cbaz&	operator=( const cbaz&	rhs ){
		debug_printf( "cbaz::operator=( const cbaz& )\n" );
		if( this != &rhs ){
			m_a = rhs.m_a;
			m_b = rhs.m_b;
			m_c = rhs.m_c;
		}
		return	*this;
	}
	~cbaz(){
		debug_printf( "cbaz::~cbaz()\n" );
	}
};
inline cbaz	make_baz( const char*	a, const char*	b, const char*	c ){
	cbaz	baz( a, b, c );
	return	baz;	// コピーができなくても move できるので影響なし
}

出力例

inline void	print_baz( const cbaz&	baz ){
	debug_printf( "baz: a = \"%s\", b = \"%s\", c = \"%s\"\n", baz.a(), baz.b(), baz.c());
}
int	main(){
	print_baz( make_baz( "1", "2", "3" ));
	return	0;
}
// ローカル変数を作成
cbaz::cbaz( "1", "2", "3" )
// 戻り値へ move
cbaz::cbaz( cbaz&& )
// ローカル変数を破棄
cbaz::~cbaz()
// 戻り値を使用
baz: a = "1", b = "2", c = "3"
// 戻り値を破棄
cbaz::~cbaz()

暗黙にはコピーも move もできないクラス

暗黙にはコピーも move もできないクラスというのは、コピーコンストラクタにも move コンストラクタにも explicit が指定されているクラスのことです
本当にコピーも move もできないクラスはどうやっても戻り値にすることはできないのでそれとは違います

明示的に指定すればコピーも move もできるクラスですが、戻り値の場合はその明示的にするということができないため、通常はこのようなクラスを戻り値の型に指定することはできません
このようなクラスを戻り値にしたいという場合には、一時的に戻り値を保持するようなクラスを用意する必要があります

// 暗黙にはコピーも move もできない
class cqux {
	std::string	m_a;
	std::string	m_b;
	std::string	m_c;
public:
	const char*	a() const { return	m_a.c_str(); }
	const char*	b() const { return	m_b.c_str(); }
	const char*	c() const { return	m_c.c_str(); }
	
	cqux():m_a(),m_b(),m_c(){
		debug_printf( "cqux::cqux()\n" );
	}
	cqux( const char*	a_, const char*	b_, const char*	c_ ):m_a(a_),m_b(b_),m_c(c_){
		debug_printf( "cqux::cqux( \"%s\", \"%s\", \"%s\" )\n", a(), b(), c());
	}
	explicit cqux( cqux&&	rhs ):
	m_a(std::move(rhs.m_a)),
	m_b(std::move(rhs.m_b)),
	m_c(std::move(rhs.m_c))
	{
		debug_printf( "cqux::cqux( cqux&& )\n" );
	}
	cqux&	operator=( cqux&&	rhs ){
		debug_printf( "cqux::operator=( cqux&& )\n" );
		m_a = std::move(rhs.m_a);
		m_b = std::move(rhs.m_b);
		m_c = std::move(rhs.m_c);
		return	*this;
	}
	explicit cqux( const cqux&	rhs ):
	m_a(rhs.m_a),
	m_b(rhs.m_b),
	m_c(rhs.m_c)
	{
		debug_printf( "cqux::cqux( const cqux& )\n" );
	}
	cqux&	operator=( const cqux&	rhs ){
		debug_printf( "cqux::operator=( const cqux& )\n" );
		if( this != &rhs ){
			m_a = rhs.m_a;
			m_b = rhs.m_b;
			m_c = rhs.m_c;
		}
		return	*this;
	}
	~cqux(){
		debug_printf( "cqux::~cqux()\n" );
	}
};

// 暗黙にはコピーも move もできないのでエラー
inline cqux	make_qux( const char*	a, const char*	b, const char*	c ){
	cqux	qux( a, b, c );
	return	qux;
	return	std::move(qux);	// 明示的に move しても cqux( cqux&& ) を呼んでくれたりはしない
}

// 一時的に戻り値を保持するクラス
class return_qux {
	cqux	m_value;
public:
	operator const cqux& () const {
		return	m_value;
	}
	return_qux( cqux&&	rhs ):
	m_value(std::move(rhs))
	{
		debug_printf( "return_qux::return_qux( cqux&& )\n" );
	}
	return_qux( return_qux&&	rhs ):
	m_value(std::move(rhs.m_value))
	{
		debug_printf( "return_qux::return_qux( return_qux&& )\n" );
	}
	~return_qux(){
		debug_printf( "return_qux::~return_qux()\n" );
	}
	return_qux() = delete;
	return_qux( const return_qux&	rhs ) = delete;
	return_qux&	operator=( const return_qux&	rhs ) = delete;
	return_qux&	operator=( return_qux&&	rhs ) = delete;
};
// 別のクラス内に一時的に保持してもらうことで qux を return できるようになる
inline return_qux	make_qux( const char*	a, const char*	b, const char*	c ){
	cqux	qux( a, b, c );
	return	std::move(qux);	// return_qux( cqux&& ) のために明示的な move が必要
}

出力例

inline void	print_qux( const cqux&	qux ){
	debug_printf( "qux: a = \"%s\", b = \"%s\", c = \"%s\"\n", qux.a(), qux.b(), qux.c());
}
int	main(){
	print_qux( make_qux( "1", "2", "3" ));
	return	0;
}
// ローカル変数を作成
cqux::cqux( "1", "2", "3" )
// 戻り値へ move
cqux::cqux( cqux&& )
return_qux::return_qux( cqux&& )
return_qux::return_qux( return_qux&& )
// ローカル変数を破棄
cqux::~cqux()
// 戻り値を使用
qux: a = "1", b = "2", c = "3"
// 戻り値を破棄
return_qux::~return_qux()
cqux::~cqux()

このように一時的に戻り値を保持してくれるクラスを使うとあたかも cqux を戻り値として返しているかのようにできますが、一時オブジェクトの寿命に気を付けてください

次の例は一見問題なさそうに見えますが、print_qux に渡されている qux の寿命は既に尽きています
qux は一時オブジェクトの中にある m_value を参照していますが、一時オブジェクトの寿命は qux の宣言されている行の ; までです

const cqux&	qux = make_qux( "1", "2", "3" );
print_qux( qux ); // 解放後アクセス!!
// ローカル変数を作成
cqux::cqux( "1", "2", "3" )
// 戻り値へ move
cqux::cqux( cqux&& )
return_qux::return_qux( cqux&& )
// ローカル変数を破棄
cqux::~cqux()
// 戻り値を破棄
return_qux::~return_qux()
cqux::~cqux()
// 戻り値を使用(=解放後アクセス!!)
qux: a = "", b = "", c = ""

また現行の C++ では operator. をオーバーロードすることはできないので次のようなことはできません
qux は cqux ではなく return_qux なので使用可能なメソッドを持ちません

auto&&	qux = make_qux( "1", "2", "3" );
debug_printf( "qux: a = \"%s\", b = \"%s\", c = \"%s\"\n", qux.a(), qux.b(), qux.c()); // エラー

このように本当に透過的に使いたいという場合には、cqux をメンバに持つのではなく、次の例のように基底クラスにします

class return_qux : public cqux {
public:
	return_qux( cqux&&	rhs ):
	cqux(std::move(rhs))
	{
		debug_printf( "return_qux::return_qux( cqux&& )\n" );
	}
	return_qux( return_qux&&	rhs ):
	cqux(std::move(rhs))
	{
		debug_printf( "return_qux::return_qux( return_qux&& )\n" );
	}
	~return_qux(){
		debug_printf( "return_qux::~return_qux()\n" );
	}
	return_qux() = delete;
	return_qux( const return_qux&	rhs ) = delete;
	return_qux&	operator=( const return_qux&	rhs ) = delete;
	return_qux&	operator=( return_qux&&	rhs ) = delete;
};
auto&&	qux = make_qux( "1", "2", "3" );
debug_printf( "qux: a = \"%s\", b = \"%s\", c = \"%s\"\n", qux.a(), qux.b(), qux.c()); // 問題なし

また cqux が基底クラスである場合には先の例は合法になります
qux は一時オブジェクトの中にあるメンバの参照ではなく、一時オブジェクト自身の参照になるので、一時オブジェクトの寿命が延長されます

const cqux&	qux = make_qux( "1", "2", "3" );
print_qux( qux ); // 問題なし

このようにすれば暗黙にコピーも move もできないクラスでも戻り値にすることはできるのですが、ただ、暗黙に move できるようにすればこんな小細工をしなくても済むので、第一には move コンストラクタに付いている explicit を外せないかどうかを検討すべきでしょう

コピーも move もできないクラス

コピーも move もできないクラスというのは、コピーコンストラクタと move コンストラクタが = delete; されていたり private だったりするクラスのことです
このような本当にコピーも move もできないクラスはどうやっても戻り値にすることはできません

このようなクラスはシングルトンのために使ったりします
シングルトンであるということはその唯一のインスタンスのリファレンスを返すだけで足りるはずなので、このようなクラスを戻り値にしたいという場合には何かがおかしいという徴です
自分が本当は何をしようとしていたのかをもう一度よく考えてみるといいと思います