ムーブで何ができる?
コピーについて紹介してきたところで、ここでいよいよムーブです。上のshallow_copy_null.cppの例で見てきたように、コピー後にオリジナルを残す必要がない場合は、ポインタの受け渡しで済ませた方が、実行効率が上がります。そこで、オリジナルを残すコピーに対して、実行効率の向上を目的に移動で済ませるムーブという考え方がC++ 11で生まれました。ムーブを利用すると、shallow_copy_null.cppの例のようにポインタを使うことなく実体の移動を記述できます。ここからは、このムーブを掘り下げていきましょう。
左辺値と右辺値[C++11]
ムーブを語るにおいて、最初に押さえておきたいのが、C++ 11より導入された左辺値(lvalue)、右辺値(rvalue)という考え方です。字面だけ見ると、二項演算子の両脇にある、左辺、右辺とその値というところでしょうか。代入操作を思い浮かべると分かりますが、左辺は代入によって値が置き換わってそのまま存在し続けますが、右辺は代入に使われたら用済みとなるケースが多いです(リテラル値や関数の戻り値など)。このようにC++ 11からは、スコープの範囲で生存する値(名前のある値)を左辺値、有効なのはその場限りですぐに破棄される値(名前のない値)を右辺値として区別することになりました。
int a = 100, b = 200; a = 200; // aは左辺値、200は右辺値 b; // 変数を単に置いても左辺値 300; // リテラルを単に置いても右辺値 b = a; // bは左辺値、aも左辺値だが右辺値に変換できる a = a + 400; // a + 400の計算結果は右辺値 a = func(); // 関数の戻り値は右辺値
実際に演算子の左辺にあるか、右辺にあるかということは無関係で、値の性質を表現したものといえるでしょうか。今回は日本語になっていますが、字面のイメージと実際の意味が若干異なるので、これも難しいですね。
右辺値参照[C++11]
値には左辺値と右辺値がある!では参照も同じじゃない?と思った方は鋭いです。そこで、「右辺値参照」です。右辺値参照は、従来の参照(第2回でちょっとだけ紹介しました)と区別するためにC++11で導入されました。では従来の参照はどうなったのかというと、「左辺値参照」と呼ばれるようになっています。従来の参照は左辺値の参照に使われるので、左辺値参照というわけですね。
左辺値参照(すなわち従来の参照)を指定するときは、「int&」というように「型名+&」としていました。右辺値参照を指定するときは「int&&」というように「型名+&&」となります。「&&」と&が1個増えるわけですね。紛らわしいですが、論理積の演算子とは違いますからね。以下は左辺値参照と右辺値参照の記述例ですが、特に右辺値参照では右辺値200を変数rで参照できるという点に注目です。
int a = 100; // 整数型変数の宣言 int& l = a; // lはaの左辺値参照 int&& r = 200; // rは200の右辺値参照 cout << l << endl; // 実行結果:100 cout << r << endl; // 実行結果:200
これらを引数の型に指定することで、参照の種類によって異なるオーバロードを定義することができます。これはすなわち次のコンストラクタと代入演算子の使い分けに結びつきます。ちなみに、右辺値参照でも左辺値参照でも、値をType&のように参照型とすることを「値をType&に束縛する」「拘束する」(ともにrestriction)というように表現しているようです。これもちょっとどころかかなり理解が難しいですよね。しかし、上の例のように右辺値200が後もrで参照できることから、「200がrに束縛される」という感じが少しは伝わるでしょうか。
ムーブコンストラクタとムーブ代入演算子[C++11]
クラスのコピーでは、初期化時にはコピーコンストラクタが、代入時にはコピー代入演算子が用いられます。コピーを許すクラスでは、基本的にこの2つが実装されており、インスタンスの生成時や代入時にコピーが実行されることになっています(これらが実装されていないクラスでは、基本的にコピーはできません)。
これまで見てきた通り、ムーブしたい場合はコピーと同じタイミングと方法で行います。そこでC++11では、ムーブコンストラクタとムーブ代入演算子というものが使えるようになりました。これらが実装されたクラスは、ムーブをサポートします。
コピーとムーブをどう区別するのか?ということですが、これには上記の右辺値参照が活躍します。コンストラクタの引数や代入演算子の右辺が右辺値参照で解決できる場合には、それはムーブできる(すなわち値は捨てても良い、もう使わない)と判断してムーブコンストラクタおよびムーブ代入演算子が呼び出されます。
以下のリストは、クラスにおけるコピーとムーブのコンストラクタと、代入演算子の定義例です。
class MyClass { public: // コンストラクタ MyClass(const MyClass &source); // コピー MyClass(MyClass &&source); // ムーブ // 代入演算子 MyClass& operator=(const MyClass &source); // コピー MyClass& operator=(MyClass &&source); // ムーブ };
コピーコンストラクタにおいては引数がconst修飾されていますが、ムーブコンストラクタでは引数の値が書き換えられることもあるのでconst修飾されません(あとで紹介しますが、スマートポインタはその典型的なケースです)。シャローコピーの例で示したように、ムーブ後にムーブ元のポインタをnullptrで置き換えるケースが相当します。代入演算子も同様です。
右辺値へのキャスト[C++11]
ムーブを試してみましょう。ディープコピーを、ムーブで書き直してみたのが以下のリストです。
vector<int> v(1000); auto w = vector<int>(std::move(v)); // vを右辺値化する //v[0] = 123; // vは無効なので実行時にエラーとなる w[0] = 456; cout << w[0] << endl; // 実行結果:456
wの初期化で、コンストラクタの引数にvを直接与えるのではなく、std::move関数の戻り値を与えています。move関数は、左辺値を右辺値にキャストします。これにより、コンパイラはvが右辺値であると判断して、vector<T>クラスのムーブコンストラクタを呼び出します。ムーブコンストラクタの呼び出しで結果としてvは使えなくなる(nullptrが入る)ので、その後は代入や参照などの一切の操作ができなくなります(そのようなコードを書くことはできますが、正常に動きません!)。move関数の呼び出しは、これを分かりやすい形でプログラマに伝えられるということです。
なお、move関数はその名前に反して右辺値にキャストするだけで、ムーブ自体は行いません。戻り値をコンストラクタや代入演算子などに与えない限りは何も起きません。