C++の基礎 - 右辺値と左辺値

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要

C言語では、代入演算子=の左側にあるものを左辺値(lvalue)右側(rvalue)にあるものを右辺値として決められている。
C++では、左辺値は名前を持つオブジェクト(&演算子でアドレスを取得できる)右辺値は名前を持たない一時オブジェクトのことである。

ここでは、右辺値、左辺値、右辺値参照、左辺値参照、ムーブ、ユニバーサル参照、完全転送の意味を記載する。


右辺値 (rvalue)

定義:

一時的な値やメモリ上の特定のアドレスを持たないオブジェクトを指す。


特徴:

リテラル、一時オブジェクト、非参照の戻り値を持つ関数呼び出し等が該当する。

通常、代入演算子の右側にのみ現れる。
&演算子を使用してアドレスを取得することはできない。(C++11以降の一部の例外を除く)

例えば、メソッドの戻り値、式の結果等は右辺値である。


同じ式でも、コンテキストにより左辺値にも右辺値にもなり得る。
また、右辺値参照 (&&) の導入により、右辺値の扱いが大きく変わり、ムーブセマンティクスが可能になった。

C++11以降、右辺値の概念は「純粋な右辺値 (prvalue)」と「xvalue」に細分化された。

 class X;
 
 int getValue();
 
 int main()
 {
    X();                 // クラスのコンストラクタは右辺値
    int z = getValue();  // getValue関数の戻り値は右辺値
 
    int i = 1;               // 変数iは左辺値, 1は右辺値
    i + 1;                   // 式i + 1は右辺値
    // int *ptr = &(5 + 3);  // エラー: 右辺値のアドレスは取得できない
 }


右辺値は左辺値に変換できないため、以下はコンパイルエラーとなる。

 int main()
 {
    int x;        // 変数xは左辺値
    (x + 1) = 9;  // コンパイルエラー
 }



左辺値 (lvalue)

定義:

メモリ上の特定のアドレスを持つオブジェクトを指す。


特徴:

名前を持つ変数や、参照、デリファレンス演算子(*)を使用した式等が該当する。
代入演算子の左側に現れることができる。 (ただし、これは必須条件ではない)

&演算子を使用して、そのアドレスを取得できる。


例えば、変数に代入されている値は左辺値である。

 class X;
 
 int main()
 {
    int i;          // 変数iは左辺値
    X x;            // クラスのインスタンスは左辺値
 
    X *ptr_X = &x;  // ポインタ変数は左辺値
 
    int *ptr = &x;  // 変数xは左辺値であるため、アドレスを取得することができる
    *ptr     = 20;  // 変数*ptrは左辺値
 }



右辺値と左辺値の比較

右辺値と左辺値の比較を以下に示す。

 // xは左辺値、1は右辺値
 int x = 1;
 
 // yは左辺値、式x + 1は右辺値、xは左辺値
 int y = x + 1;
 
 // zは左辺値、func関数の戻り値は右辺値、式y + 2も右辺値
 int z = func(y + 2);
 
 struct Point
 {
    int x = 0;
    int y = 0;
 };
 
 // Pointクラスのインスタンスptは左辺値、Pointクラスのコンストラクタは右辺値
 Point pt = Point();
 
 // 1は右辺値
 1;
 
 // func関数の戻り値は右辺値、zは左辺値
 // 戻り値はコピーも束縛もしていないため、以降使用できない
 func(z);



左辺値参照と右辺値参照

左辺値参照は、左辺値を束縛(※)すること、または、その参照変数のことである。
左辺値参照は右辺値を参照することができないが、constが付加された左辺値参照は右辺値を参照することができる。

※束縛
右辺値・左辺値に関わらず、参照を初期化することを束縛するという。
参照 = 束縛対象といった代入の形、または、束縛対象を関数の実引数にする形で束縛される。
参照を使用する時、束縛対象(参照元)の値を返す。

また、参照に参照を代入する時、代入元の束縛対象を代入先の束縛対象にする。

 class X {};
 
 void f(X &x) {}
 
 void g(const X &x) {}
 
 int main()
 {
    X x;
    f(x);    // コンパイル可能
             // コピーコンストラクタの引数は左辺値参照
    g(x);    // コンパイルエラー
             // Xクラスのインスタンスは左辺値のため、constが付加された引数には指定できない
 
    g(X());  // コンパイル可能
             // constが付加された引数は右辺値を参照できる
 }


右辺値参照は、右辺値を束縛(※)すること、または、その参照変数のことである。
右辺値参照は、<データ型>&& <変数名>と記述する。

右辺値参照は型であるため、左辺値になることができる。

 int func()
 {
    return 20;
 }
 
 int main()
 {
    int&& i = 10;       // 右辺値10を右辺値参照変数iに束縛
    int&& j = func();   // func関数の戻り値(右辺値)を右辺値参照変数jに束縛
 
    std::cout << i << std::endl;  // 10
    std::cout << j << std::endl;  // 20
 
    i = j;              // 右辺値参照は左辺値になることもできるため、コンパイル可能
 
    std::cout << i << std::endl;  // 20
    std::cout << j << std::endl;  // 20
 }


以下に、左辺値参照と右辺値参照の比較を示す。

 // []は束縛対象(参照元), <>はコピーされた値
 int x      = 1;                  // xは左辺値<1>           1は右辺値
 int& lref1 = x;                  // lrefは左辺値参照[x]    xは左辺値
 int& lref2 = lref;               // lref2は左辺値参照[x]  lref1は左辺値参照[x]
 // int& lref3 = 1;               // コンパイルエラー
                                  // lref3は左辺値参照     1は右辺値
 int y      = lref1;              // yは左辺値<xの値>      lrefは左辺値参照[x]
 
 int&& rref1 = 1;                 // rref1が右辺値参照[1]   1は右辺値
 // int&& rref2 = x;              // コンパイルエラー
                                  // rref2が右辺値参照     xは左辺値
 // int&& rref3 = lref1;          // コンパイルエラー
                                  // rref3が右辺値参照     lrefは左辺値参照
 // int&& rref4 = rref1;          // コンパイエルエラー
                                  // rref4が右辺値参照     rrefは右辺値参照
 int&& rrefm = std::move(lref1);  // rrefmが右辺値参照      std::move(lref1)は右辺値
 int w       = rref1;             // wが左辺値<1>          rref1は右辺値参照[1]


以下に、constを付加した左辺値参照も示す。
一般的に、constを付加した右辺値参照は使用されない。(constを付加したオブジェクトの内部を変更すべきではないため)

 // []は束縛対象(参照元), <>はコピーされた値
 int x             = 1
 
 const int &clref1 = x;       // clref1がconst左辺値参照[1]  xは左辺値
 const int &clref2 = lref1;   // clref2がconst左辺値参照[1] lref1は左辺値参照[1]
 const int &clref3 = 2;       // constが付加された左辺値参照は、右辺値を参照できる
                              // clref3がconst左辺値参照[2] 2は右辺値
 int z             = clref1;  // zが左辺値<1>  clrefはconst左辺値参照[1]



ムーブ

C++11以降、代入とコンストラクタにはコピーとムーブがある。
コピーは、データを全てコピーするため重い処理になるが、ムーブは、ポインタとサイズ情報のコピーのみを行うため軽い。
ただし、ムーブ元のオブジェクトのデータは、ムーブ後は不定になるため、そのオブジェクトは使用できなくなる。

ムーブ先のオブジェクトがムーブに対応している場合、左辺値をstd::moveメソッドの引数に指定して、
代入、または、コンストラクタの引数に指定するだけでムーブができる。

特に、右辺値参照はムーブで使用されることが多い。

※std::moveメソッド
std::moveメソッドは、左辺値を右辺値にキャストする。
右辺値なので、代入した場合は、operartor=(const type&)ムーブオペレータoperartor=(type&&)が使用される。
ムーブオペレータは、ポインタの挿げ替えとムーブ元オブジェクトを無効にする機能が実装されている。

例えば、aの値をbに代入する時、インスタンスのコピーが行われて、メモリ上に2つのインスタンスが存在することになる。
もし、aが指すインスタンスが以降で使用されない場合、aのポインタをbが持つだけでよいため、コピー操作は必要無い。

 class Test;
 
 Test a = Test();
 Test b = a;
 // aはこれ以降使用しない


ある変数が持つオブジェクトを別の変数に割り当てて、その変数からはオブジェクトを使わないようにする操作をムーブという。
ムーブは、ムーブコンストラクタやムーブ演算子を使用して表現する。

ムーブコンストラクタやムーブ演算子の引数に、右辺値参照が現れる。
ムーブに対して、コピー演算子やコピーコンストラクタの引数は、左辺値参照となる。

std::moveメソッドは、左辺値を右辺値にキャストするものである。

 #include <iostream>
 #include <utility>
 
 class Counter
 {
 private:
    int m_cnt;
 
 public:
    Counter() : m_cnt(0)
    {
       std::cout << "Default" << std::endl;
    }
 
    Counter(const Counter &c)
    {
       std::cout << "Copy" << std::endl;
    }
 
    Counter(Counter &&c)
    {
       m_cnt = c.getCnt();
       std::cout << "Move" << std::endl;
    }
 
    ~Counter() {}
 
    int getCnt() {return m_cnt;}
 };
 
 int main()
 {
    Counter c1,                 // 引数無しのコンストラクタ
            c2;                 // 引数無しのコンストラクタ
    Counter c3(c1);             // コピーコンストラクタ
    Counter c4(std::move(c2));  // ムーブコンストラクタ
 
    std::string str1 = "abc";
 
    std::string str2 = str1;             // str2をコピー
    std::string str3 = std::move(str1);  // str3にstr1の内容をムーブする。以降str1の内容は不定となる
    std::string str4(std::move(str3));   // str4にstr3の内容をムーブする。以降str3の内容は不定となる
 
    return 0;
 }



ユニバーサル参照(&&で左辺値も束縛できる特別な例外)

auto変数やtemplate変数の&&による参照は、右辺値だけでなく、左辺値も束縛できる。
ユニバーサル参照は、次のセクションに記載する完全転送に関わる。

 // []は束縛対象(参照元), <>はコピーされた値
 
 // g関数に渡した引数が左辺値の場合は左辺値参照となり、右辺値の場合は右辺値参照となる。
 template <typename T>
 void g(T&& uref) {}
 
 int x             = 1;       // xは左辺値                      1は右辺値
 int& lref1        = x;       // lref1が左辺値参照              xは左辺値
 const int& clref1 = x;       // clref1はconst左辺値参照        xは左辺値
 
 int&& rref1       = 1;       // rref1が右辺値参照[1]           1は右辺値
 // int&& rref2    = x;       // コンパイルエラー
                              // rref2が右辺値参照             lref1が左辺値参照
 // int&& rref3    = lref1;   // コンパイルエラー
                              // rref3が右辺値参照             lref1が左辺値参照
 // int&& rref4    = rref1;   // コンパイルエラー
                              // rref4が右辺値参照             rref1が右辺値参照(左辺値)
 // int&& rref5    = clref1;  // コンパイルエラー
                              // rref5が右辺値参照             clref1がconst左辺値参照
 
 auto&& uref1      = 1;       // uref1がユニバーサル参照[1]     1は右辺値 
 auto&& uref2      = x;       // uref2がユニバーサル参照[x]     xは左辺値 
 auto&& uref3      = lref1;   // uref3がユニバーサル参照[x]     lref1が左辺値参照[x]
 auto&& uref4      = rref1;   // uref4がユニバーサル参照[1]     rref1が右辺値参照[1]
 auto&& uref5      = clref1;  // uref5がユニバーサル参照[x]     clref1がconst左辺値参照[x]
 
 g(1);                        // g()の引数はユニバーサル参照   1は右辺値
 g(x);                        // g()の引数はユニバーサル参照   xは左辺値
 g(lref1);                    // g()の引数はユニバーサル参照  lref1は左辺値参照
 g(rref1);                    // g()の引数はユニバーサル参照   rref1が右辺値参照
 g(clref1);                   // g()の引数はユニバーサル参照   clref1がconst左辺値参照



完全転送

完全転送とは、ユニバーサル参照が束縛した値において、右辺値・左辺値の型情報も保持して転送すること。
完全転送を行うには、std::forwardメソッドを使用する。

 // 左辺値参照関数
 void ref(int& a)
 {
    std::cout<< "左辺値参照" <<std::endl;
 }
 
 // 右辺値参照関数
 // 引数bは右辺値のみ受け取る
 void ref(int&& b)
 {
    std::cout << "右辺値参照" << std::endl;
 }
 
 // 左辺値参照関数が呼び出される
 template <typename T>
 void h(T&& uref)
 {
    ref(uref);
 }
 
 // 右辺値参照版・左辺値参照版のどちらが呼ばれるかは引数により変わる
 template <typename T>
 void h_with_forward(T&& uref)
 {
    ref(std::forward<T>(uref));
 }
 
 std::string str = "abc";
 int x           = 1;
 
 h(std::move(str));               // std::move(str)は右辺値  出力:左辺値参照
 h(1);                            // 1は右辺値  出力:左辺値参照
 h(x);                            // xは左辺値  出力:左辺値参照
 
 h_with_forward(std::move(str));  // std::move(str)は右辺値  出力:右辺値参照
 h_with_forward(1);               // 1は右辺値  出力:右辺値参照
 h_with_forward(x);               // xは左辺値  出力:左辺値参照



対応表

下表に、変数Aに変数Bを代入する時(A = B)の対応表を示す。

A\B 左辺値 const左辺値 左辺値参照 const左辺値参照 右辺値 右辺値参照 ユニバーサル参照
左辺値 コピー コピー コピー コピー コピー コピー コピー
const左辺値 コピー コピー コピー コピー コピー コピー コピー
左辺値参照 束縛 × 束縛 × × 束縛 束縛
const左辺値参照 束縛 束縛 束縛 束縛 束縛 束縛 束縛
右辺値参照 × × × × 束縛 × ×
ユニバーサル参照 束縛 束縛 束縛 束縛 束縛 束縛 束縛