C++の左辺値と右辺値
C++の左辺値と右辺値について勉強したのでまとめておく.
参考にしたサイト
本の虫: rvalue reference 完全解説
C++11 Tutorial – thispointer.com
左辺値と右辺値
C言語では,左辺値は=
の左側にあるのが左辺値(lvalue)で,右側(rvalue)にあるものが右辺値として決められている.
C++ではそうではない.シンプルに書くと,
左辺値は名前を持つオブジェクト(よって&
演算子でアドレスを取得することが出来る)で,
右辺値は名前を持たない一時オブジェクトのこと.
左辺値の例
class X; int main() { int i; X x; X* ptr_X = &x; }
など.
右辺値の例
class X; int func(); int main() { X(); func(); int i = 1; //これは左辺値 i + 1; }
関数の戻り値や式の結果などは右辺値.
左辺値を右辺値に変換することはOK.右辺値を左辺値に変換することは出来ない.
つまり下のコードはコンパイルが通らない:
int main() { int x; (x + 1) = 9; }
参照の場合
C++98では,参照と言うと左辺値に対する参照を意味する.
普通の左辺値参照は右辺値を参照することが出来ない.
しかし,constな左辺値参照は右辺地を参照することが出来る.
class X {}; void f(X &x) {} void g(X const &x) {} int main() { X x; f(x); // OK g(x); // Error g( X() ); // OK. const referenceはrvalueを参照できる }
右辺値はimmutable?
C++98ではconst lvalue referenceはrvalueを参照することが出来た.これと辻褄が合うようにするには,rvalueはimmutableになっている気がする.
実際は,intやdoubleといった基本型の右辺値はimmutableで,クラスなどユーザー定義された型はmutableになる.
基本型のrvalue
int func() { return 1;} int main() { int i = 0; (i + 1) = 10; // i + 1というrvalueに10を代入することは出来ない. func() = 2; // 関数の戻り値に2を代入することは出来ない. }
ユーザー定義クラスのrvalue
class Counter { public: Counter() : m_cnt(0) {} void incr() { m_cnt++;} private: m_cnt; }; Counter func() { return Counter();} int main() { Counter cnt; cnt.incr(); // OK Counter().incr(); // OK func().incr(); // OK Counter *ptr = &func(); // Error }
右辺値としてのCounter
オブジェクトに対して,incrメソッドはクラスメンバを変更するにもかかわらず,incrメソッドを呼び出すことは可能.
右辺値参照
C++98では,constな左辺値参照は右辺値を参照することが出来た.
つまりC++98までは左辺値参照と右辺値参照は区別されていなかった.
C++11では,これらを明確に区別するため,右辺値参照が導入された.
右辺値参照とは,その名の通り右辺値への参照である.
int*
が「intへのポインタ」という型であるように,「〇〇への右辺値参照」も型である.
右辺値参照はX&&
とかく.
右辺値参照自体は1種の型なので,左辺値になることができる
また(左辺値・右辺値にかかわらず)参照を初期化することを束縛するとか言ったりするらしい.
int func() {return 20;} int main() { int&& i = 10; // 10という右辺値を右辺値参照変数iに束縛 int&& j = func(); // funcの戻り値を右辺値参照変数jに束縛 cout << i << endl; //10 cout << j << endl; //20 i = j; //右辺値参照は左辺値なので,OK cout << i << endl; //20 cout << j << endl; //20 }
右辺値参照の使いみち
右辺値参照はムーブに使われる.
ムーブとは,あるオブジェクト(左辺値のオブジェクトでもいいし,一時オブジェクトでもよい)を,コピーすることなく他の変数に割り当ててしまうという操作である.
例えば,次のようなコードを考えてみる
class Test; Test a = Test(); Test b = a; // aはこれ以降使わない
aからbに代入するときに,オブジェクトのコピーが行われ,メモリ上に2つのTestオブジェクトが存在することになる.もし,aが指し示すオブジェクトが以降のコードで使われない場合,単純にaのオブジェクトへのポインタをbが持つようにするだけでよく,コピー操作は必要ない.
このように,ある変数が持つオブジェクトを別の変数に割り当て,その変数からはオブジェクトを使わないようにする操作を,ムーブという.
ムーブは,ムーブコンストラクタやムーブ演算子を使って表現する.ムーブコンストラクタやムーブ演算子の引数に,右辺値参照が現れる.
ムーブに対して,コピー演算子やコピーコンストラクタの引数は,左辺値参照となる.
#include <iostream> #include <utility> using namespace std; class Counter { public: Counter() : m_cnt(0) { cout << "Default" << endl; } Counter(Counter &&c) { m_cnt = c.getCnt(); cout << "Move" << endl; } Counter(Counter &c) { cout << "Copy" << endl; } ~Counter() {} int getCnt() {return m_cnt;} private: int m_cnt; }; int main() { Counter c, c1; // 引数なしのコンストラクタが2回呼ばれる Counter c2(c); // コピーコンストラクタが呼ばれる Counter c3(move(c1)); // ムーブコンストラクタが呼ばれる. return 0; }
std::move
さっきのコードで出てきたmove
という関数は,渡された左辺値オブジェクトを右辺値にキャストするものである.
move (utility) - cpprefjp C++日本語リファレンス