この記事はIS18er Advent Calendar 2017の3日目の記事として書かれました。
記事公開直前にしてめちゃくちゃ似てるタイトルの記事を見つけて萎えた
C++11から右辺値参照というものが追加されましたが、僕は初めてこれを見たとき割と???となった記憶があります
それ以来使うタイミングもなく今まで来てしまいましたが、モヤモヤなまま終わるのも良くないなということで、このタイミングに自分用のまとめを兼ねて紹介してみようと思います
違う情報書いて詐欺してる可能性も無きにしも非ずですが、間違ってたら教えてください(´・ω・`)
目次
まずよく見かける参照について
とりあえずコードを見てみましょう
#include <iostream> using namespace std; int main() { int x = 0; int& lvalue = x; lvalue = 10; cout << "x = " << x << endl; return 0; }
出力結果:
x = 10
lvalueを変更したのにxの値が変わっていますね、不思議
参照とは、変数に新しい名前をつける機能のことであり、上のコードであれば、xにlvalueという別名をつけたことになります
一応値渡しとポインタ渡しについても触れてみます
値渡し
#include <iostream> using namespace std; int main() { int x = 0; int y = x; y = 10; cout << "x = " << x << endl; return 0; }
出力結果:
x = 0
ポインタ渡し
#include <iostream> using namespace std; int main() { int x = 0; int* px = &x; *px = 10; cout << "x = " << x << endl; return 0; }
出力結果:
x = 10
当たり前やろ!!!って言ってくれると嬉しいです
ポインタ渡しは、出力結果が参照渡しと同じですね
両方とも変数x本体の値を変更しているわけですが、参照渡しでピンと来なかった人は「参照はポインタ渡しを書きやすくしたもの」だと思ってください(適当
右辺値
上に細かい定義が載っていますが、要するに一時的なオブジェクトという認識でいいと思います
一時的なオブジェクトってなんやねん!!って言われそうなので例をみてみましょう
#include <iostream> using namespace std; int main() { 1+2; int x = 0; x; return 0; }
上の例でいうと1+2が右辺値で、xが左辺値に当たります
変数xはプログラム中で式の左辺(つまり代入される側)に回れますが、1+2は左辺に来ることはできないですよね
もう一個例を見てみます
#include <iostream> using namespace std; class Obj{ public: Obj() { cout << "constructor" << endl; } ~Obj() { cout << "destructor" << endl; } }; int main() { cout << "start" << endl; Obj(); cout << "end" << endl; return 0; }
出力結果:
start constructor destructor end
上のコードだとObj()が右辺値に当たるわけですが、出力結果の通り、Obj()はコンストラクタが呼ばれるとすぐにデストラクタが呼ばれていますね
こういうわけで一時オブジェクトとも言われます
右辺値と左辺値を区別する理由
さて、C++03以前では右辺値と左辺値は明確に区別されておらず、例えば
Obj a = Obj("data.txt"); a = Obj("new_data.txt");
このような場合2行目の処理は
- まず一時オブジェクトObj("new_data.txt")を作成
- 次に一時オブジェクトをconst左辺値参照というもので参照してaへのコピー処理を実行する
という手順になっていました
const左辺値参照というのは値の変更を禁止した左辺値参照です、一時オブジェクトは左辺に来れない(つまり値の変更ができない)からですね
しかしこの処理、、、なんか無駄じゃない???
Obj("new_data")というところでコンストラクタが呼ばれていて、おそらくファイルの読み込みやらなんやらをする処理だと思いますが、せっかく読み込んだものをaにコピーして、処理が終わると一時オブジェクトObj("new_data.txt")は消える、、、、完全に無駄です
一時オブジェクトはどうせそこで死ぬのなら、いっそのことObj("new_data.txt")内のリソースをコピーせず直接変数aに渡しちゃえば早いですよね
こういった状況が右辺値と左辺値を区別したくなるモチベーションになります
右辺値参照
ここまで分かっていればあとはプログラムの話になります☺️
実は冒頭に出て来た参照は左辺値参照と言い、名前の通り左辺値を参照する機能です
となると右辺値参照は右辺値の参照ですね
右辺値参照をすることで、一時オブジェクトの寿命は参照をした変数と同じだけ伸びます*1
#include <iostream> using namespace std; int main() { int a = 0; int& b = 0; int& c = a; int&& d = 0; int&& e = a; return 0; }
右辺値参照は&&でできます
上のプログラムの場合、「int& b = 0」と「int&& e = a」がエラーになりますが、それぞれ「右辺値を左辺値参照した」「左辺値を右辺値参照した」からですね
というわけで右辺値参照はこれだけです!!!!
実際に右辺値参照を使う場面
Obj a = Obj("data.txt"); a = Obj("new_data.txt");
これはさっきも出て来たコードですね
C++11以降では右辺値参照が使えるので、Objクラスのコピーコンストラクタ、代入演算子*2を左辺値参照版、右辺値参照版*3の2つ用意しておくことで、より効率的なコードを書くことができます
std::move
ところで次の状況を考えてみてください
Obj a = Obj("data.txt") Obj b = a // aは以後使わない // 処理いろいろ
Obj b = aでは左辺値を代入していますが、aは以後使わないらしいので効率的に右辺値参照をしたい気がしますよね
こんな時は標準ライブラリのstd::move関数を使ってみると、なんと左辺値を右辺値に変換して右辺値参照ができます
Obj a = Obj("data.txt") Obj b = std::move(a) // aは以後使わない // 処理いろいろ
std::moveによってaは右辺値として扱われ右辺値参照ができますが、代償としてこれ以降aをいじってはいけません
これだけみるとstd::moveは魔法みたいですが、実装を見てみると単にキャストをしています
その他右辺値参照を使った技術にstd::forwardというものがありますが、ちょっと使う状況の説明が長くなるので気になる人はググったり参考リンク見てみてください
まとめ
- 右辺値は一時的なオブジェクト、変更ができない
- C++03以前でも右辺値を参照することはできたけれど、右辺値の参照と左辺値の参照を区別したいので右辺値参照が生まれた
- 以降使わないオブジェクトを代入する時など、コピーより実体を移動させた方が効率がいい場面があり、そんな時に右辺値参照を使う
- std::moveで左辺値を右辺値として扱える
参考リンク
一時オブジェクトの寿命と右辺値参照、ムーブセマンティクスのお話 - Qiita
右辺値参照・ムーブセマンティクス - cpprefjp C++日本語リファレンス
右辺値、左辺値などの細かい定義 - Qiita
本の虫: rvalue reference 完全解説
*1:よく延命措置って言われてる気がしなくもない
*2:http://yohshiy.blog.fc2.com/blog-entry-303.html C++ のコピーコンストラクターと代入演算子 | プログラマーズ雑記帳
*3:右辺値参照の場合、コピーではなく実体の移動なのでムーブコンストラクタなんて言います