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

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動
編集の要約なし
270行目: 270行目:
  g(clref1);                  // g()の引数はユニバーサル参照  clref1がconst左辺値参照
  g(clref1);                  // g()の引数はユニバーサル参照  clref1がconst左辺値参照
  </syntaxhighlight>
  </syntaxhighlight>
<br><br>
== 完全転送 ==
完全転送とは、ユニバーサル参照が束縛した値において、右辺値・左辺値の型情報も保持して転送すること。<br>
完全転送を行うには、<code>std::forward</code>メソッドを使用する。<br>
<syntaxhighlight lang="c++">
// 左辺値参照関数
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は左辺値  出力:左辺値参照
</syntaxhighlight>
<br><br>
== 対応表 ==
下表に、変数Aに変数Bを代入する時(A = B)の対応表を示す。<br>
<center>
{| class="wikitable" style="background-color:#fefefe;"
|-
! style="background-color:#00ffff;" | A\B
! style="background-color:#00ffff;" | 左辺値
! style="background-color:#00ffff;" | const左辺値
! style="background-color:#00ffff;" | 左辺値参照
! style="background-color:#00ffff;" | const左辺値参照
! style="background-color:#00ffff;" | 右辺値
! style="background-color:#00ffff;" | 右辺値参照
! style="background-color:#00ffff;" | ユニバーサル参照
|-
| 左辺値 || コピー || コピー || コピー || コピー || コピー || コピー || コピー
|-
| const左辺値 || コピー || コピー || コピー || コピー || コピー || コピー || コピー
|-
| 左辺値参照 || 束縛 || × || 束縛 || × || × || 束縛 || 束縛
|-
| const左辺値参照 || 束縛 || 束縛 || 束縛 || 束縛 || 束縛 || 束縛 || 束縛
|-
| 右辺値参照 || × || × || × || × || 束縛 || × || ×
|-
| ユニバーサル参照 || 束縛 || 束縛 || 束縛 || 束縛 || 束縛 || 束縛 || 束縛
|}
</center>
<br><br>
<br><br>


__FORCETOC__
__FORCETOC__
[[カテゴリ:C++]]
[[カテゴリ:C++]]

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

概要

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左辺値参照



完全転送

完全転送とは、ユニバーサル参照が束縛した値において、右辺値・左辺値の型情報も保持して転送すること。
完全転送を行うには、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左辺値参照 束縛 束縛 束縛 束縛 束縛 束縛 束縛
右辺値参照 × × × × 束縛 × ×
ユニバーサル参照 束縛 束縛 束縛 束縛 束縛 束縛 束縛