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