左辺値や右辺 値などの式のカテゴリは、C ++言語の使用の実際的な側面よりも、C ++言語の基本的な理論的概念に関連しています。 このため、多くの経験豊富なプログラマでさえ、彼らが何を意味するかについて漠然とした考えを持っています。 この記事では、これらの用語の意味を可能な限り簡単に説明し、理論を実用的な例で希釈します。 すぐに予約します。この記事では、表現のカテゴリの最も完全で厳密な説明を提供するふりをしません。詳細については、ソースに直接お問い合わせください:C ++言語標準。
この記事にはかなり多くの英語の用語が含まれます。これは、ロシア語への翻訳が難しいものもあれば、異なるソースで異なる方法で翻訳されるものもあるためです。 したがって、英語の用語をイタリック体で強調して 、頻繁に示します 。
ちょっとした歴史
左辺値と右辺 値という用語は、Cに登場しました。値ではなく式を参照しているため、最初は用語に混乱が生じていたことに注意してください。 歴史的に、 左辺値は代入演算子の左にできる値であり、 右辺値は右にしかできない値です。
lvalue = rvalue;
しかし、そのような定義は本質をいくらか単純化し、歪めます。 C89標準は、 左辺値をオブジェクトロケーターとして定義しました。 識別可能なメモリ位置を持つオブジェクト。 したがって、この定義に適合しないものはすべて右辺値カテゴリに含まれていました。
ビャルンは救助に急いで
C ++では、特に右辺値リンクと移動セマンティクスの概念を導入したC ++ 11標準の採用後に、式カテゴリの用語が非常に強力に進化しました。 新しい用語の出現の歴史は、興味深いことにStraustrupの記事「New」Value Terminologyで説明されています。
新しい、より厳密な用語は、2つのプロパティに基づいています。
- アイデンティティ( identity )の存在-つまり、2つの式が同じエンティティを参照しているかどうかを理解できるパラメータ(メモリ内のアドレスなど)。
- 移動する機能(から移動可能 )-移動のセマンティクスをサポートします。
アイデンティティを表現する式はglvalue ( 一般化された値 )という用語で一般化され、ローミング式はrvalueと呼ばれます。 これら2つのプロパティの組み合わせにより、3つの主要な式のカテゴリが特定されました。
| アイデンティティを持っている | アイデンティティの欠如 |
---|
移動できません | 左辺値 | - |
移動できます | xvalue | 価値 |
実際、C ++ 17標準では、 コピーの省略という概念が導入されています。コンパイラがオブジェクトのコピーと移動を回避できる状況を正式に規定しています。 この点で、 prvalueは必ずしも移動されるとは限りません。 詳細と例については、 こちらをご覧ください 。 ただし、これは式のカテゴリの一般的なスキームの理解には影響しません。
最新のC ++標準では、カテゴリの構造はそのようなスキームの形式で与えられます。

一般的な用語で、カテゴリのプロパティ、および各カテゴリに含まれる言語表現を調べてみましょう。 各カテゴリの以下の式のリストは完全であるとは見なせないことに注意してください。より正確で詳細な情報については、C ++標準を直接参照してください。
glvalue
glvalueカテゴリの式には、次のプロパティがあります。
- 暗黙的にprvalueに変換できます。
- ポリモーフィックである可能性があります。つまり、静的型と動的型の概念は意味があります。
- void型にすることはできません-これは、アイデンティティを持つというプロパティから直接続きます。void型の式には、それらを互いに区別するようなパラメータがないためです。
- たとえば、 前方宣言の形式の不完全な型を持つことができます(特定の式で許可されている場合)。
右辺値
右辺値カテゴリの式には、次のプロパティがあります。
- メモリ内で右辺値アドレスを取得することはできません。これは、アイデンティティプロパティの欠如から直接生じます。
- 代入演算子または複合代入演算子の左側に置くことはできません。
- 定数左辺値リンクまたは右辺 値リンクを初期化するために使用できますが、オブジェクトの寿命はリンクの寿命まで延長されます。
- 2つのオーバーロードされたバージョンを持つ関数を呼び出すときに引数として使用すると、1つは定数左辺値リンクを受け入れ、もう1つは右辺値リンクを受け入れ、 右辺値リンクを受け入れるバージョンが選択されます。 移動セマンティクスの実装に使用されるのは、このプロパティです:
class A { public: A() = default; A(const A&) { std::cout << "A::A(const A&)\n"; } A(A&&) { std::cout << "A::A(A&&)\n"; } }; ......... A a; A b(a); // A(const A&) A c(std::move(a)); // A(A&&)
技術的には、A &&は右辺値であり、定数左辺値参照と右辺 値参照の両方を初期化するために使用できます。 しかし、このプロパティのおかげで、あいまいさはなく、 右辺値参照を受け入れるコンストラクタオプションが受け入れられます。
左辺値
プロパティ:
- すべてのglvalueプロパティ(上記を参照);
- アドレスを取得できます(組み込みの単項演算子
&
)。 - 変更可能な左辺値は、代入演算子または複合代入演算子の左側に配置できます。
- 左辺値 (定数と非定数の両方)への参照を初期化するために使用できます。
次の式は左辺値カテゴリに属します。
- 任意のタイプの変数、関数、またはクラスフィールドの名前。 変数が右辺値参照であっても、式内のこの変数の名前は左辺値です。
void func() {} ......... auto* func_ptr = &func; // : auto& func_ref = func; // : int&& rrn = int(123); auto* pn = &rrn; // : auto& rn = rrn; // : lvalue-
- 左辺値参照、または左辺値参照の型への変換式を返す関数またはオーバーロードされた演算子を呼び出します。
- 組み込み代入演算子、複合代入演算子(
=
、 +=
、 /=
など)、組み込みの事前インクリメントおよび事前インクリメント( ++a
、-- --b
)、組み込みのポインター逆参照演算子( *p
); - オペランドの1つが左辺値配列である場合、インデックス(
a[n]
またはn[a]
)によるアクセスの組み込み演算子。 - 関数または関数への右辺値参照を返すオーバーロードされた演算子の呼び出し。
"Hello, world!"
などの文字列リテラル 。
文字列リテラルは、C ++の他のすべてのリテラルとは、それが左辺値 (不変ではありますが)であるという点で正確に異なります。 たとえば、そのアドレスを取得できます。
auto* p = &”Hello, world!”; // ,
価値
プロパティ:
- すべての右辺値プロパティ(上記を参照);
- ポリモーフィックにすることはできません。静的および動的な式タイプは常に一致します。
- 不完全な型にすることはできません( void型を除き、これについては以下で説明します)。
- 抽象型を持つことも、抽象型の要素の配列にすることもできません。
次の式はprvalueカテゴリに属します。
- リテラル(文字列を除く)、たとえば
42
、 true
またはnullptr
; - 非参照(
str.substr(1, 2)
、 str1 + str2
、 it++
)または非参照型への変換式(例えば、 static_cast<double>(x)
、 std::string{}
、 (int)42
); - 組み込みのポストインクリメントとポストデクリメント(
a++
、 b--
)、組み込みの数学演算( a + b
、 a % b
、 a & b
、 a << b
など)、組み込みの論理演算( a && b
、 a || b
!a
など)、比較演算( a < b
、 a == b
、 a >= b
など)、アドレスを取得する組み込み演算( &a
); - このポインター。
- リスト項目;
- クラスではない場合の非定型テンプレートパラメータ。
- ラムダ式、たとえば
[](int x){ return x * x; }
[](int x){ return x * x; }
xvalue
プロパティ:
- すべての右辺値プロパティ(上記を参照);
- すべてのglvalueプロパティ(上記を参照)。
xvalueカテゴリの式の例:
- 右辺値参照を返す関数または組み込み演算子を呼び出します。たとえば、 std :: move(x) ;
実際、 std :: move()を呼び出した結果、メモリ内のアドレスを取得したり、そのアドレスへのリンクを初期化することはできませんが、同時に、この式は多態的です:
struct XA { virtual void f() { std::cout << "XA::f()\n"; } }; struct XB : public XA { virtual void f() { std::cout << "XB::f()\n"; } }; XA&& xa = XB(); auto* p = &std::move(xa); // auto& r = std::move(xa); // std::move(xa).f(); // “XB::f()”
- オペランドの1つが右辺値配列である場合、インデックス(
a[n]
またはn[a]
)によるアクセスの組み込み演算子。
特別な場合
コンマ演算子
組み込みのコンマ演算子の場合、式カテゴリは常に第2オペランドの式カテゴリと一致します。
int n = 0; auto* pn = &(1, n); // lvalue auto& rn = (1, n); // lvalue 1, n = 2; // lvalue auto* pt = &(1, int(123)); // , rvalue auto& rt = (1, int(123)); // , rvalue
ボイド式
voidを返す関数の呼び出し、 voidへの型変換式、および例外のスローはprvalueカテゴリの式と見なされますが、参照の初期化や関数の引数として使用することはできません。
三項比較演算子
式のカテゴリの定義a ? b : c
a ? b : c
ケースは自明ではなく、すべて2番目と3番目の引数( b
およびc
)のカテゴリに依存します。
b
またはc
がvoid型の場合 、式全体のカテゴリとタイプは別の引数のカテゴリとタイプに対応します。 両方の引数がvoid型の場合、結果はvoid型のprvalueです。b
とc
が同じ型のglvalueである場合、結果は同じ型のglvalueです。- それ以外の場合、結果はprvalueです。
三項演算子については、引数bおよびcに暗黙的な変換を適用できるいくつかの規則が定義されていますが、これは記事の範囲をやや超えています。興味がある場合は、標準の条件演算子[expr.cond]セクションを参照することをお勧めします。
int n = 1; int v = (1 > 2) ? throw 1 : n; // lvalue, .. throw void, n ((1 < 2) ? n : v) = 2; // lvalue, , ((1 < 2) ? n : int(123)) = 2; // , .. prvalue
クラスと構造のフィールドとメソッドへの参照
am
およびp->m
という形式の式(ここでは組み込み演算子->
について説明しています)には、次の規則が適用されます。
m
が列挙要素または非静的クラスメソッドの場合、式全体がprvalueと見なされます (ただし、そのような式ではリンクを初期化できません)。a
が右辺値で 、 m
が非参照型の非静的フィールドである場合、式全体がxvalueカテゴリにあります。- それ以外の場合は、 左辺値です。
クラスメンバーへのポインター( a.*mp
およびp->*mp
)の場合、ルールは同様です。
mp
がクラスメソッドへのポインタである場合、式全体がprvalueと見なされます 。a
が右辺値で 、 mp
がデータフィールドへのポインタである場合、式全体がxvalueを参照します 。- それ以外の場合は、 左辺値です。
ビットフィールド
ビットフィールドは低レベルのプログラミングに便利なツールですが、それらの実装は式カテゴリの一般的な構造から多少外れています。 たとえば、ビットフィールドへの呼び出しは、割り当て演算子の左側に存在する可能性があるため 、 左辺値と思われます。 同時に、ビットフィールドのアドレスを取得したり、それらによって非定数リンクを初期化することはできません。 ビットフィールドへの定数参照を初期化できますが、オブジェクトの一時コピーが作成されます。
ビットフィールド[class.bit]
const T&型の参照の初期化子がビットフィールドを参照する左辺値である場合、参照はビットフィールドの値を保持するために一時的に初期化されます。 参照はビットフィールドに直接バインドされていません。
struct BF { int f:3; }; BF b; bf = 1; // OK auto* pb = &b.f; // auto& rb = bf; //
結論の代わりに
冒頭で述べたように、上記の説明は完全であると主張するのではなく、表現のカテゴリの一般的なアイデアを提供するだけです。 このビューは、標準のセクションとコンパイラエラーメッセージの理解をわずかに向上させます。