「C++の基礎 - 右辺値と左辺値」の版間の差分

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動
編集の要約なし
6行目: 6行目:
<br><br>
<br><br>


== 右辺値 ==
== 右辺値と左辺値 ==
メソッドの戻り値、式の結果等は右辺値である。<br>
メソッドの戻り値、式の結果等は右辺値である。<br>
  <syntaxhighlight lang="c++">
  <syntaxhighlight lang="c++">
31行目: 31行目:
  }
  }
  </syntaxhighlight>
  </syntaxhighlight>
<br><br>
<br>
 
== 左辺値 ==
左辺値とは、変数に代入されている値のことである。<br>
左辺値とは、変数に代入されている値のことである。<br>
  <syntaxhighlight lang="c++">
  <syntaxhighlight lang="c++">
45行目: 43行目:
     X *ptr_X = &x;  // ポインタ変数は左辺値
     X *ptr_X = &x;  // ポインタ変数は左辺値
  }
  }
</syntaxhighlight>
<br>
以下に、右辺値と左辺値の比較を示す。<br>
<syntaxhighlight lang="c++">
int x = 1;            // xは左辺値、1は右辺値
int y = x + 1;        // yは左辺値、式x + 1は右辺値、xは左辺値
int z = func(y + 2);  // zは左辺値、func関数の戻り値は右辺値、式y + 2も右辺値
struct Point
{
    int x = 0;
    int y = 0;
};
Point pt = Point();  // Pointクラスのインスタンスptは左辺値、Pointクラスのコンストラクタは右辺値
1;                  // 1は右辺値
func(z);            // func関数の戻り値は右辺値
                      // 戻り値はコピーも束縛もしていないため、以降使用できない
  </syntaxhighlight>
  </syntaxhighlight>
<br><br>
<br><br>


== 左辺値参照 ==
== 左辺値参照と右辺値参照 ==
左辺値参照は右辺値を参照することができないが、constが付加された左辺値参照は右辺値を参照することができる。
左辺値参照は、左辺値を束縛(※)すること、または、その参照変数のことである。<br>
左辺値参照は右辺値を参照することができないが、<u>constが付加された左辺値参照は右辺値を参照する</u>ことができる。<br>
<br>
<u>※束縛</u><br>
<u>右辺値・左辺値に関わらず、参照を初期化することを束縛するという。</u><br>
<u>参照 = 束縛対象といった代入の形、または、束縛対象を関数の実引数にする形で束縛される。</u><br>
<u>参照を使用する時、束縛対象(参照元)の値を返す。</u><br>
<br>
<u>また、参照に参照を代入する時、代入元の束縛対象を代入先の束縛対象にする。</u><br>
  <syntaxhighlight lang="c++">
  <syntaxhighlight lang="c++">
  class X {};
  class X {};
69行目: 94行目:
  }
  }
  </syntaxhighlight>
  </syntaxhighlight>
<br><br>
<br>
 
右辺値参照は、右辺値を束縛(※)すること、または、その参照変数のことである。<br>
== 右辺値参照 ==
右辺値参照は、<データ型>&& <変数名>と記述する。<br>
右辺値参照とは、右辺値への参照である。
<br>
右辺値参照は、<データ型>&& <変数名>と記述する。
右辺値参照は型であるため、左辺値になることができる。<br>
 
右辺値参照は型であるため、左辺値になることができる。
また、右辺値・左辺値に関わらず、参照を初期化することを束縛するという。
  <syntaxhighlight lang="c++">
  <syntaxhighlight lang="c++">
  int func()
  int func()
88行目: 110行目:
     int&& j = func();  // func関数の戻り値(右辺値)を右辺値参照変数jに束縛
     int&& j = func();  // func関数の戻り値(右辺値)を右辺値参照変数jに束縛
   
   
     cout << i << endl;  // 10
     std::cout << i << std::endl;  // 10
     cout << j << endl;  // 20
     std::cout << j << std::endl;  // 20
   
   
     i = j;              // 右辺値参照は左辺値になることもできるため、コンパイル可能
     i = j;              // 右辺値参照は左辺値になることもできるため、コンパイル可能
   
   
     cout << i << endl;  // 20
     std::cout << i << std::endl;  // 20
     cout << j << endl;  // 20
     std::cout << j << std::endl;  // 20
  }
  }
  </syntaxhighlight>
  </syntaxhighlight>
<br>
<br>
右辺値参照は、ムーブで使用される.<br>
以下に、左辺値参照と右辺値参照の比較を示す。<br>
ムーブとは、あるオブジェクト(左辺値や一時オブジェクト等)をコピーすることなく他の変数に割り当てる操作である。<br>
<syntaxhighlight lang="c++">
// []は束縛対象(参照元), <>はコピーされた値
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]
</syntaxhighlight>
<br>
以下に、constを付加した左辺値参照も示す。<br>
一般的に、constを付加した右辺値参照は使用されない。(constを付加したオブジェクトの内部を変更すべきではないため)<br>
<syntaxhighlight lang="c++">
// []は束縛対象(参照元), <>はコピーされた値
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]
</syntaxhighlight>
<br><br>
 
== ムーブ ==
C++11以降、代入とコンストラクタにはコピーとムーブがある。<br>
コピーは、データを全てコピーするため重い処理になるが、ムーブは、ポインタとサイズ情報のコピーのみを行うため軽い。<br>
ただし、ムーブ元のオブジェクトのデータは、ムーブ後は不定になるため、そのオブジェクトは使用できなくなる。<br>
<br>
ムーブ先のオブジェクトがムーブに対応している場合、左辺値を<code>std::move</code>メソッドの引数に指定して、<br>
代入、または、コンストラクタの引数に指定するだけでムーブができる。<br>
<br>
特に、右辺値参照はムーブで使用されることが多い。<br>
<br>
<u>※std::moveメソッド</u><br>
<u><code>std::move</code>メソッドは、左辺値を右辺値にキャストする。</u><br>
<u>右辺値なので、代入した場合は、<code>operartor=(const type&)</code>、<code>ムーブオペレータoperartor=(type&&)</code>が使用される。</u><br>
<u>ムーブオペレータは、ポインタの挿げ替えとムーブ元オブジェクトを無効にする機能が実装されている。</u><br>
<br>
<br>
例えば、aの値をbに代入する時、インスタンスのコピーが行われて、メモリ上に2つのインスタンスが存在することになる。<br>
例えば、aの値をbに代入する時、インスタンスのコピーが行われて、メモリ上に2つのインスタンスが存在することになる。<br>
155行目: 224行目:
     Counter c3(c1);            // コピーコンストラクタ
     Counter c3(c1);            // コピーコンストラクタ
     Counter c4(std::move(c2));  // ムーブコンストラクタ
     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;
     return 0;
  }
  }
</syntaxhighlight>
<br><br>
== ユニバーサル参照(&&で左辺値も束縛できる特別な例外) ==
<code>auto</code>変数や<code>template</code>変数の<code>&&</code>による参照は、右辺値だけでなく、左辺値も束縛できる。<br>
<syntaxhighlight lang="c++">
// []は束縛対象(参照元), <>はコピーされた値
template <typename T>
void g(T&& urefa) {}
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左辺値参照
  </syntaxhighlight>
  </syntaxhighlight>
<br><br>
<br><br>

2021年3月12日 (金) 17:09時点における版

概要

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

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


右辺値と左辺値

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

 class X;
 
 int func();
 
 int main()
 {
    X();        // クラスのコンストラクタは右辺値
    func();     // func関数の戻り値は右辺値
 
    int i = 1;  // 変数iは左辺値, 1は右辺値
    i + 1;      // 式i + 1は右辺値
 }


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

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


左辺値とは、変数に代入されている値のことである。

 class X;
 
 int main()
 {
    int i;  // 変数iは左辺値
    X x;    // Xクラスのインスタンスは左辺値
 
    X *ptr_X = &x;  // ポインタ変数は左辺値
 }


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

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



左辺値参照と右辺値参照

左辺値参照は、左辺値を束縛(※)すること、または、その参照変数のことである。
左辺値参照は右辺値を参照することができないが、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変数の&&による参照は、右辺値だけでなく、左辺値も束縛できる。

 // []は束縛対象(参照元), <>はコピーされた値
 
 template <typename T>
 void g(T&& urefa) {}
 
 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左辺値参照