備忘録です。
ムーブ、ムーブセマンティクスの概念、ムーブコンストラクタの話を図を交えて解説します。
本記事は、「ムーブという概念は聞いたことがあるんだけど実際どういうものかわからない」という人が対象(のつもり)です。
左辺値と右辺値
ムーブ、ムーブセマンティクスの話をする前に非常に重要な左辺値と右辺値の話をします。
C++では左辺値と右辺値が明確に区別されます。
int i = 1;
例えばこのコードでは、int
型で宣言されているi
が左辺値で、1
が右辺値になります。
もう少し詳しく説明すると、左辺値はint i
のような名前付きのオブジェクトのことで、右辺値はすぐに破棄されてしまうような一時的なオブジェクトのことです。
下に右辺値の例を挙げます。
1; // リテラル
f(); // 参照がない返り値
Object o;
std::move(o) // std::moveを適応させたlvalue
std::move
という見慣れない命令が出てきましたがとりあえず置いておいて...
右辺値は基本的に評価された時点で破棄されてしまいます。
そのため右辺値を生かしておくには右辺値への参照を持つ右辺値参照型という型で束縛する必要があります。
int&& r = 1; // 右辺値1を束縛できた!
右辺値参照型はT&&
で宣言できます。
一方でObject o
のような名前付きのオブジェクトのことを**左辺値(lvalue)**と呼びます。
左辺値は関数のスコープを外れるまで破棄されません。
ちなみに右辺値参照型は右辺値への参照を持つための型のため、左辺値です。
このため、すぐ破棄されてしまう右辺値を生かすことが可能になります。
左辺値を右辺値にキャストする std::move
では、さきほど登場したstd::move
についてです。
std::move
は左辺値を右辺値にキャストします。
#include <utility>
Object o;
std::move(o) // Object o を Object&& oにキャスト!
これでObject o
は右辺値へキャストされました。
左辺値を右辺値にして何が嬉しいの?
std::move
は左辺値を右辺値にキャストしますがこれは何が嬉しいのでしょうか?
関数のスコープから外れない限り内容が破棄されないことが保証されている左辺値を、一時オブジェクトと同様の右辺値参照に変更する意味はあるのでしょうか?
では、次のような例を考えてみましょう。
#include <string>
std::string str("...") // 大量の文字列データを持っているstring型変数
/* strに対して色々な処理を行う */
std::string str_after = str; // 色々な処理を施したstrをstr_afterにコピー!
/* これ以降strに対して処理は行わない */
このプログラムでは大量の文字列データを持ったstr
に色々な処理を施した後に str_after
という変数にその内容をコピーしています。
つまり str_after
はstr
のデータをコピーするためにstr
のデータ分のメモリを確保する必要があります。
動的にメモリを確保するにはOSに必要な分だけのメモリ領域をリクエストする必要があり、C++の処理の中でも時間がかかってしまう部分です。
もしかしたら巨大なデータ領域を新たにリクエストすると、メモリの容量が足りずnullptrを返されるかもしれません。(自分は遭遇したことはありませんが)
プログラム中のstr
はコピー後使われないようです。
もう使わないデータならば、下図のようにデータを移動できればいいのに...
このとき活躍するのがC++11から新たにクラスに定義できるようになったムーブコンストラクタとムーブ代入演算子です。
ムーブコンストラクタとムーブ代入演算子は一般的に以下のように宣言されます。
// ムーブコンストラクタ
Test (Test &&right) {}
// ムーブ代入演算子
Test &operator=(Test &&right) {}
ムーブコンストラクタとムーブ代入演算子は引数に右辺値への参照を取ります。
右辺値ということはこの処理が終わり次第、破棄されてしまう可能性が高いオブジェクトです。
引数にそのようなオブジェクトがあるのですからそのままコピーするのはもったいないです。
どうせ捨てられるならその値の所有権を奪ってもいいはずです。
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
class Test {
private:
char *p;
public:
Test(): p(new char[1000]) {};
~Test() {
delete [] p;
}
Test(Test const &right) {
*this = right;
}
Test& operator=(Test const &right) {
if (this != &right) {
p = new char[1000]; // 新しいメモリ領域を用意
memcpy(p, right.p, 1000); // コピー
}
return *this;
}
Test (Test &&right) {
*this = std::move(right);
}
Test& operator=(Test &&right) {
printf("move assigned!\n");
if (this != &right) {
delete [] p; // 自分のメモリ領域を消して...
p = right.p; // 相手のメモリ領域を譲り受ける
right.p = nullptr; // 相手のポインタはnullptrに!
}
return *this;
}
};
int main() {
Test lval1;
Test lval2;
Test rval(lval1); // copy constructor
rval = std::move(lval2); // move代入
}
上に示したのが、ムーブコンストラクタとムーブ演算代入を実装したものです。
ムーブでは相手のポインタを自分のポインタに代入し、さらに相手のポインタをnullptr
で初期化しています。
これにより新たにメモリ領域を作らず所有権を奪うことができました。
つまり、左辺値にstd::move
を適応させることは適応元の左辺値をムーブ元にしても良いと宣言することと同意です。
この処理以降つかわない!みたいなオブジェクトに使用しましょう。
ただし、ムーブコンストラクタは常に生成されるわけではないことに注意です。
クラスがムーブに対応しているかをチェックすることは非常に大事です。
また、現在のSTLのコンテナは基本的にムーブに対応していますが、ムーブコンストラクタにnoexcept
指定がないとコピーコンストラクタが呼ばれてしまうものもある(vectorとか)などの罠もあるため注意が必要です。
ムーブセマンティクス
ムーブセマンティクスとは、左辺値と右辺値が明確に区別されたことによって、ムーブコンストラクタとムーブ代入など値の受け渡し方法に幅ができたこと(と僕は解釈しています)。
まとめ
- C++において左辺値と右辺値は明確に区別される。
- 右辺値参照は左辺値
-
std::move
は左辺値を右辺値にキャストする。 - ムーブコンストラクタとムーブ代入演算子は右辺値参照を引数にとる。
参考文献
Effective Modern C++
C++のムーブと完全転送を知る
cpprefjp
std::forwardと完全転送の仕様をまとめようと思っていたらmoveについて書いていた...