ホームに戻る
出典 :
右辺値参照・ムーブセマンティクス - cpprefjp C++日本語リファレンス 7分でわかる右辺値参照 - Qiita 右辺値参照とムーブコンストラクタの使い方 - C++ プログラミング もう怖くないC++11の右辺値参照 - ややプログラム紀行 C++ のムーブを理解する
関連 :
C++11 参照型 コピーコンストラクタ スマートポインタ 型推論 演算子オーバーロード 関数のdefault・delete宣言
目次 :

ムーブ(ムーブセマンティクス)とは

あるオブジェクトから他のオブジェクトにリソースを明け渡すこと。これにより、リソースの所有権がムーブ先のオブジェクトに移動する。
C++11以降で使用可能。

何故ムーブが必要か

オブジェクトの代入とコンストラクタには、コピーとムーブの両方が存在し、通常はコピーが選択される。
コピーはデータの全てをコピーするので(オブジェクトの構成によっては)比較的重い処理となるが、
ムーブは通常、ポインタとサイズ情報のコピーのみを行うので非常に軽い処理で済む。
このため、コピー元オブジェクトが残っている必要が無い場合には、ムーブを用いることでパフォーマンスの向上が期待できる。

概念の比較 : ファイルのコピー・ムーブ

画像
オブジェクトのコピー・ムーブはファイルのコピー・ムーブと似ている。
同一ドライブ内の別フォルダにファイルをムーブする場合、フォルダ情報が変更されるのみでファイルの実体はそのままであるため、高速で処理が行われる。
しかしコピーの場合は、ファイルの実体がコピー先に複製されるため、ファイルが大きいほど処理に時間がかかる。

右辺値参照

C++11で追加された、右辺値を書き換え可能とする仕組みのことを「右辺値参照」と呼ぶ。これはオブジェクトのムーブを実現するために必要となる。

前提 : 左辺値と右辺値

を指す。右辺値は代入で左辺に格納されなければそこで寿命を終え、使えなくなってしまう
代入文の「左辺となり得る」のが左辺値、それ以外は右辺値と捉えればよい。
int x = 1; //< x : 左辺値 //< 1 : 右辺値 int y = x + 1; //< y, x : 左辺値 //< x+1 : 右辺値 int z = f(y + 2); //< z, y : 左辺値 //< y+2, f() の戻り値 : 右辺値 struct Point { int x = 0; int y = 0; }; Point pt = Point(); //< pt : 左辺値 //< Point() (コンストラクタ)の戻り値 : 右辺値 1; //< 1 : 右辺値 f(z); //< f() の戻り値 : 右辺値

左辺値参照と右辺値参照

束縛 : 対象を参照変数に関連付けること。束縛対象に別名を付与することでもある。

T& で宣言される型は左辺値を束縛する「左辺値参照」であり、
C++11で追加された T&&右辺値を束縛する「右辺値参照」である。
// 左辺値参照 T& int x = 1; //< x は左辺値 int& lref1 = x; //< lref1 は左辺値参照 ⇒ x を束縛 int& lref2 = lref1; //< lref2 は左辺値参照 ⇒ lref1 を束縛 ⇒ x を束縛 // int& lref3 = 1; //< lref3 は左辺値参照 ⇒ 右辺値を束縛しようとしているためエラー int y = lref1; //< y には lref1 で参照される値が代入される ⇒ 1 // 右辺値参照 T&& int&& rref1 = 9; //< rref1 は右辺値参照 ⇒ 9 を束縛 // int&& rref2 = x; //< rref2 は右辺値参照 ⇒ 左辺値を束縛しようとしているためエラー // int&& rref3 = lref1; //< rref3 は右辺値参照 ⇒ 左辺値参照(左辺値)を束縛しようとしているためエラー // int&& rref4 = rref1; //< rref4 は右辺値参照 ⇒ 右辺値参照(左辺値)を束縛しようとしているためエラー int w = rref1; //< w には rref1 で参照される値が代入される ⇒ 9

オブジェクトのムーブ

オブジェクトのムーブを行う際は std::move() 関数を使用する。<utility> のインクルードが必要。
std::move() 関数に渡されたオブジェクト(左辺値)は右辺値参照にキャストされる。
渡された(ムーブ元の)オブジェクトは右辺値となり、それ以降は使える保証がなくなる。
#include <string> #include <utility> void main() { std::string x = "Hello, world!"; // 何も起こらない std::move(x); // 実際に x から y へ文字列がムーブされる // (これ以降 x の値は不定) std::string y = std::move(x); }
上記は std::move() の基本的な使用例である。
注意が必要な点として、std::move() 関数は「このオブジェクトはこれ以降使用しない」ことの明示に過ぎず、
実際のムーブは std::move() 関数の戻り値(右辺値参照)をムーブコンストラクタ・ムーブ代入演算子(後述)に渡すことで行われる。

所有権の移動

クラスによってはコピーは禁止されているが、ムーブは可能(同じものが複数存在してはならない)といったものが存在する。
そのようなクラスでは、ムーブは「所有権の移動」を表す。
スマートポインタが代表的で、所有権が移動した後ムーブ元は無効値( nullptr など)となることが保証されている。
#include <utility> #include <memory> void main() { // std::unique_ptr (スマートポインタ) p の初期化 std::unique_ptr<int> p(new int(1)); // ムーブの実行 // ⇒ p が指し示す先の所有権が q に移り p は nullptr になる std::unique_ptr<int> q = std::move(p); }

ムーブコンストラクタとムーブ代入演算子

ムーブの実現は、ムーブコンストラクタ・ムーブ代入演算子によって行われる。
これらはいずれも右辺値参照を引数に取る。
#include <utility> #include <algorithm> // クラス定義 class large_class { private: char* ptr ; public: // (デフォルト)コンストラクタ large_class() { ptr = new char[1000]; } // コピーコンストラクタ large_class( const large_class& r ) { // コピー元オブジェクトの ptr の内容をコピー ptr = new char[1000] ; std::copy( r.ptr, r.ptr + 1000, ptr ); } // ムーブコンストラクタ large_class( large_class&& r ) { // ポインタの挿げ替え ptr = r.ptr; // ムーブ元のオブジェクトは nullptr に r.ptr = nullptr; } // ムーブ代入演算子 large_class& operator=( large_class&& r ) { // 既存バッファの破棄 delete [] ptr; // ポインタの挿げ替え ptr = r.ptr; // ムーブ元のオブジェクトは nullptr に r.ptr = nullptr; return *this; } // デストラクタ ~large_class() { delete[] ptr; } }; // main() 関数 void main() { // オブジェクト x の初期化 large_class x {}; // x をコピーした c の作成 large_class c { x }; // x をムーブした mA の作成 large_class mA { std::move( x ) }; // x を mB にムーブ代入 large_class mB {}; mB = std::move( x ); }
上記は、ムーブコンストラクタおよびムーブ代入演算子の例である。
コピー代入演算子では左辺値参照を引数に取るのに対し、
ムーブコンストラクタおよびムーブ代入演算子では右辺値参照を引数に取っている。
単に初期値(または代入右辺)に x を指定した場合はコピーとなるが、std::move() を用いた場合はムーブとなる。
ムーブ処理はポインタの挿げ替えと、ムーブ元の無効化のみが行われるため、コピーと比較して非常に高速である。
なお、標準ライブラリで提供されるクラスのほとんどはムーブコンストラクタが実装されており、ムーブが有効である。

また、これらは明示的に定義を行わなかった場合も暗黙的に定義される。このため default を指定することも可能である。
// クラス定義 class large_class { : public: // ムーブコンストラクタ large_class( large_class&& r ) = default; // ムーブ代入演算子 large_class& operator=( large_class&& r ) = default; };

ユニバーサル参照と完全転送

関数テンプレート型のパラメータ T や、型推論プレースホルダ auto に && を付与して宣言したものはユニバーサル参照(Universal Reference)と呼ばれ、
対象が左辺値であれば左辺値参照、右辺値であれば右辺値参照となる。
受け取ったパラメータを別の関数へ渡す際に、左辺値・右辺値の情報を保ったまま渡したい場合は、
ユニバーサル参照と std::forward() を組み合わせることでそれが可能となる。
この左辺値・右辺値の情報を保ったまま渡すことを完全転送(Perfect Forwarding)と呼ぶ。
#include <utility> template <typename T> void g( T ); // ユニバーサル参照を引数にとる関数 f() template <typename T> void f( T&& a ) { g( std::forward(a) ) ; }
上記のコードでは、関数 f() の引数 a がユニバーサル参照となっている。
f() では a が左辺値参照の場合は左辺値に、右辺値参照の場合は右辺値に変換して別関数 g() の実引数として渡す。
g() は値型 T としてパラメータを取るため左辺値ではコピーが、右辺値ではムーブが行われる。

余談 : ムーブセマンティクス導入の背景

ムーブセマンティクスは、C++03でもNRVO(特定の文脈でのコンストラクタの省略)や、 C++11で非推奨となった std::auto_ptr で実現されていた。
( std::auto_ptr についてはスマートポインタを参照。)
しかし、NRVOが常に機能するわけではなく、std::auto_ptr はコピーと同じ文法でムーブしていることなどの問題があったため、
C++11ではコピーと区別でき、統一的にムーブを表す文法が言語機能として整備された。