C++11 std::move 個人的まとめリベンジ

268, 2019-12-26

目次

C++11 std::move 個人的まとめの個人的なリベンジ記事。

std::move

lvalue を xvalue にキャストする。

A a, b;
b = std::move(a); // result of 'std::move(a)' is xvalue(rvalue)

参照と引用記事

検証用クラス

ideone - 検証用クラス

各コンストラクタ、オペレーターが呼ばれたら標準出力するだけのクラス。

用語

一覧

  • glvalue ... generalized lvalue(一般化lvalue)。lvalue と xvalue の総称。
  • rvalue ... right value。prvalue と xvalue の総称。
  • lvalue ... left value。関数かオブジェクト。
  • prvalue ... pure rvalue。rvalue のうち xvalue ではないもの。一時オブジェクト、リテラル値、関数のリファレンスではない戻り値など。
  • xvalue ... eXpiring value(消失値)。消失しても問題のない値。

分類

glvalue
lvalue
xvalue
rvalue
prvalue
xvalue

ほかに必要な用語

  • lvalue reference ... lvalue へのリファレンス。
  • rvalue reference ... rvalue へのリファレンス。

コードで見る用語

lvalue (glvalue)

void func() {} // func is lvalue
int num; // num is lvalue
A a; // a is lvalue
"literal"; // is lvalue
void func(int num) {} // num is lvalue

prvalue (rvalue)

0;
true;
nullptr;
A();
[](){};

xvalue (glvalue or rvalue)

A a, b;
a + b; // result of 'a + b' is xvalue
A&& ar = static_cast<A&&>(a); // result of 'static_cast<A&&>(a)' is xvalue
A&& br = std::move(b); // result of 'std::move(b)' is xvalue

lvalue reference

A a;
A& ar = a;
A const& acr = a;

rvalue reference

A a;

// Error. 'a' is lvalue. 'ar' type of rvalue reference
A&& ar = a;

// result of 'std::move(a)' is xvalue(rvalue)
A&& ar = std::move(a);

ここで'ar'は、型に&&を付けた rvalue reference だが、分類はlvalueになる。

なぜ我々は std::move() するのか

Move Semantics を利用するため。
クラスAがメンバに要素数10000の配列を持っていた場合、コピーでは要素10000個がコピーされる。

A a, b;
b = a; // 'a' is lvalue

Move Semantics を利用すると、クラスの実装にもよるが10000個のコピーはされずにポインタのすげ替えだけで済む。速い。

A a, b;
b = std::move(a); // result of 'std::move(a)' is xvalue(rvalue)

それなら全部ムーブしてくれればいいのにと思うが、Move Semantics はムーブ元を破壊して実現する。つまりムーブ元には保証が無くなる。オブジェクトの、ムーブ元の配列には nullptr が代入されているかもしれないし、ムーブ先の配列のアドレスと交換されているかもしれない。

A a, b;
b = std::move(a);
a; // 'a' is danger!

ムーブされた 'a' は今後、使えなくなる。

使えなくなるけど、再代入すれば再利用できるよ

いつ Move Semantics が働くのか

代入元が rvalue である時。

A a;
A b(std::move(a)); // result of 'std::move(a)' is xvalue(rvalue)
b = std::move(a); // result of 'std::move(a)' is xvalue(rvalue)
b = A(); // result of 'A()' is prvalue(rvalue)

rvalue を渡しても、実際にムーブするかどうかはクラスの実装による。後述の rvalue reference はムーブも出来るし、コピーも出来る。しかし std::move() でキャストして渡してる以上、ムーブされる前提で考えるのが普通だろう。

オブジェクトがムーブされる時

オブジェクトがムーブされる時、Move Semantics の為のコンストラクタ、オペレーターが呼ばれる。ムーブの実現方法は各クラスの実装による。

A(A&& other) {/* Move Constructor */}
A& operator=(A&& other) {/* Move Assignment Operator */ return *this; }

これらは特別なメンバー関数になるので、暗黙的に定義される場合と明示的に定義する場合とを考えると、かなりややこしく、頭が沸騰しそうになる。明示的に定義したら暗黙的に delete されたり、default になったりする。

struct A {
};
A a, b;
b = std::move(a); // move assignment operator

このクラスAで考えると、Move Semantics に必要なムーブコンストラクタ、ムーブ代入演算子は暗黙的に default 定義され、ムーブで呼ばれる。
それらを明示的に delete 定義すると、

struct A {
 A() {};
 A(A&& other) = delete;
 A& operator=(A&& other) = delete;
};

A a, b;
b = std::move(a); // error: overload resolution selected deleted operator '='

コンパイルエラーになる
コピーコンストラクタ、コピー代入演算子を明示的に定義するとどうなるのかというと

struct A {
 A() {};
 A(A const& other) {}
 A& operator=(A const& other) { return *this; }
};
A a, b;
b = std::move(a); // copy assignment operator

ムーブコンストラクタ、ムーブ代入演算子は暗黙的に宣言されず、ムーブ時にはコピーで処理される。
私はこの仕様を読んでいる時、プレデターと勇猛果敢に戦ったシュワちゃんの気持ちが少しだけわかりました。

明示的な定義で考えれば、ムーブではムーブ、コピーではコピー、delete 定義であれば必要によってコンパイルエラーになる。

struct A {
 A() {/* Constructor */};
 A(A const& other) {/* Copy Constructor */}
 A(A&& other) {/* Move Constructor */}
 A(A const&& other) = delete;
 A& operator=(A const& other) {/* Copy Assignment Operator */ return *this; }
 A& operator=(A&& other) {/* Move Assignment Operator */ return *this; }
 A& operator=(A const&& other) = delete;
};
A a, b;
b = std::move(a); // move assignment operator

いつ Move Semantics を利用するべきか

今後、使う予定のないオブジェクトはムーブしたほうが速く済む。例えば std::string をメンバに持つオブジェクトの構築時とか。

std::string str = "test string.";
...
A a(std::move(str)); // result of 'std::move(str)' is xvalue(rvalue)
// 'str' is danger!

実引数 str をムーブすればメンバへのコピーのコストが減る(rvalue reference と併用することで世界が平和になる)。その代わりオブジェクトの構築後、ムーブした str の保証がなくなる。

std::move() と rvalue reference

型に&&を付けると rvalue reference になる。

A a;
A&& ar = std::move(a);

a は ar にムーブされない。lvalue reference の場合を考えるとわかりやすいかも。
ar をムーブさせたい。

A a;
A&& ar = std::move(a);
A b = ar; // 'ar' is lvalue

これはムーブされない。ar は lvalue に分類されるのでムーブするにはキャストしなくてはいけない。

A a;
A&& ar = std::move(a);
A b = std::move(ar); // result of 'std::move(ar)' is xvalue(rvalue)

仮引数も lvalue だから、コンストラクタ等でもそう。

A(std::string&& str) : _str(std::move(str)) {}

std::string str = "nyannyan";
A a(std::move(str));

rvalue reference を併用すると Move Semantics の回数も減らせる。

ややこしいところ

rvalue を const lvalue reference で束縛

A a;
A const& acr = std::move(a); // Success
int const& icr = 0; // Success

rvalue なのに lvalue reference で束縛できちゃう。

const rvalue reference

A a;
A const&& b = std::move(a);
A c = std::move(b); // copy?

const rvalue reference に対応した処理が呼ばれる。

A(A const&& other) {/* Move Constructor */}
A& operator=(A const&& other) {/* Move Assignment Operator */}

const を付けてるのにムーブで破壊されても困るけど。使い道あるのかしら。

コピー省略とオーバーロード解決

以下のコードではAのコンストラクタは何回呼ばれるだろうか。

A getA() {
 A ret;
 return ret;
}
A a = getA(); // ???

一回しか呼ばれない。
これはC++の実装でコピー/ムーブ構築を省略しているからなのだそう。コピー省略と呼ばれる。手持ちのコンパイラでは一回しか呼ばれなかった。

コピー省略は、以下の場合に許されている。
...
クラスを返す関数のreturn文のオペランドの式が、関数とcatch句の仮引数を除く非volatileの自動オブジェクトの名前であり、関数の戻り値の型と自動オブジェクトの型が、ともにCV非修飾の型である場合、関数内の自動オブジェクトのコピー/ムーブが省略され、関数の戻り値のオブジェクトとして直接に構築することが許されている。
...
リファレンスで束縛されていないクラスの一時オブジェクトが、同じCV非修飾の型に、コピー/ムーブされた場合、コピー/ムーブは省略され、コピー/ムーブ先のオブジェクトに、一時オブジェクトを直接構築することが許されている。

以下のコードだとまた趣が変わる。

A getA() {
 A ret;
 return ret;
}
A a;
a = getA(); // ???

前者との違いは、

A a = getA();

↑これが

A a;
a = getA();

↑こうなった。

この場合、コンストラクタ、それからムーブかコピー代入が呼ばれる。どちらが呼ばれるかはクラスの実装による。

コピー省略できる条件がそろい、コピーされるオブジェクトがlvalueの場合、オーバーロード解決が二回行われることがある。一回目のオーバーロード解決は、オブジェクトをrvalueとして、コピーするコンストラクターを探す。一回目のオーバーロード解決が失敗するか、選択されたコンストラクターの第一引数がrvalueリファレンスではない場合、オブジェクトをlvalueとして、二回目のオーバーロード解決が行われる。

一回目のオーバーロード解決で、選択されたコンストラクターの第一引数がrvalueリファレンスではない場合というのは、たとえばconst修飾されたlvalueリファレンスが該当する。

この二回のオーバーロード解決は、たとえコピー省略をしないとしても、必ず行われる。この規定の目的は、コピー省略が行われなかったならば呼び出されるコンストラクターの、アクセス指定を調べるためである。

getA()内のローカル変数 ret はコピー省略が行われて代入文右辺に直接構築される。構築されたオブジェクトは二回オーバーロード解決が行われ、一回目は rvalue として処理される。クラスにムーブ代入演算子が定義されていれば、それが呼ばれる。rvalue に対応する処理が無ければ一回目の解決に失敗して lvalue として二回目の解決が行われる。ここの理解が怪しい

詳細はこちら:http://ezoeryou.github.io/cpp-book/C++11-Syntax-and-Feature.xhtml#class.copy
コメントで教えていただきました。yohhoy氏、ありがとうございました。

おわりに

C++ は書き手の脳内リソースを消費し、たまに気が向いたら足を撃ちぬくらしい。

投稿者名です。64字以内で入力してください。

必要な場合はEメールアドレスを入力してください(全体に公開されます)。

投稿する内容です。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク