メモ置き場

メモ置き場です.開発したものや調べたことについて書きます.

[tex: ]

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++日本語リファレンス