C++の基礎 - 右辺値と左辺値
概要
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左辺値参照 | 束縛 | 束縛 | 束縛 | 束縛 | 束縛 | 束縛 | 束縛 |
右辺値参照 | × | × | × | × | 束縛 | × | × |
ユニバーサル参照 | 束縛 | 束縛 | 束縛 | 束縛 | 束縛 | 束縛 | 束縛 |