値のカテゴリとその参照

このトピックでは、C++ に存在するさまざまなカテゴリの値 (および値への参照) について説明します。

  • glvalue
  • lvalue
  • xlvalue
  • prvalue
  • rvalue

lvaluervalue については聞いたことがあると思いますが、 ただし、このトピックで使っている表現では、それらを捉えていないかもしれません。

C++ のすべての式は、上記の 5 つのカテゴリのいずれかに属する値を生成します。 C++ 言語の機能とルールには、これらの値カテゴリとそれらの参照を適切に理解する必要がある側面があります。 これらの側面には、値のアドレスの取得、値のコピー、値の移動、別の関数への値の転送などがあります。 このトピックでは、これらの側面のすべてについて詳しくは説明しませんが、これらの側面をしっかりと理解するための基本的な情報を提供します。

このトピックの情報は、ID と移動性の 2 つの独立したプロパティによって、Stroustrup による値カテゴリの分析に関して説明されています [Stroustrup, 2013]。

lvalue は ID を持っています

値が ID を持つことはどういう意味ですか? 値のメモリ アドレスを持っている (または取得できる) 場合は、それを安全に使用すると、値には ID があります。 そうすることで、値の内容を比較する以上の操作を行うことができます。ID で比較または区別できます。

lvalue は ID を持っています。 "lvalue" の "l" は "left" の頭文字ですが (代入の左辺)、これは今では歴史的な意味しかありません。 C++ では、lvalue は代入の左辺 "または" 右辺のどちらでも使用できます。 "lvalue" の "l" は、実際にはそれらが何であるかを理解したり定義したりするのに役立ちません。 lvalue と呼ばれるものは ID を持つ値である、と理解しておくことだけが必要です。

lvalue である式の例としては、名前付きの変数や定数、または参照を返す関数などがあります。 lvalue では "ない" 式の例は、一時的な式や、値を返す関数などです。

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    std::vector<byte> vec{ 99, 98, 97 };
    std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
    int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.

    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}

確かに、lvalue には同一性があるというのは正しいですが、それは xvalue にも当てはまります。 xvalue については、このトピックの後半で詳しく説明します。 ここでは、glvalue ("generalized lvalue": 一般化された glvalue) と呼ばれる値のカテゴリがあることだけを憶えておいてください。 glvalue の集合は、lvalue(classical lvalues とも呼ばれる)と xvalue の両方を含む上位集合です。 したがって、"lvalue has identity" は true ですが、この図に示すように、ID を持つものの完全なセットは glvalue のセットです。

lvalue は ID を持っています

rvalue は移動可能であり、lvalue は移動可能ではない

しかし、glvalues ではない値があります。 つまり、メモリ アドレスを取得できない (または有効に依存 できない ) 値があります。 上記のコード例では、このような値をいくつか見ました。

信頼性の高いメモリ アドレスがない場合は、欠点のように聞こえます。 しかし、実際には、そのような値の利点は、それをコピーする(これは一般的に高コストです)のではなく、移動できる(これは一般的に低コストです)ことです。 値を移動することは、値が以前の場所に存在しなくなったことを意味します。 そのため、以前の場所でアクセスすることは避ける必要があります。 値を移動するタイミングと 方法 については、このトピックの範囲外です。 ここでは、移動可能な値は 右辺値(または 古典的な右辺値)と呼ばれることを知っておけば十分です。

"rvalue" の "r" は、"right" の頭文字です (代入の右辺)。 ただし、代入の外側でも、rvalue を使ったり、rvalue を参照したりできます。 つまり、"rvalue" の "r" は注目すべき点ではありません。 rvalue と呼ばれるものは移動可能な値である、とだけ理解しておけば十分です。

逆に、次の図に示すように、左辺値は移動できません。 もし lvalue が移動するとしたら、それはまさに lvalue の定義そのものに反します。 それは、lvalue に引き続きアクセスできることがごく当然に期待されるコードに対して予期しない問題になります。

右辺値は移動可能であり、左辺値は移動可能ではありません

lvalue を移動することはできません。 しかし、あなたが 何をしているか (移動後にアクセスしないように注意することを含む)を知っていれば、移動できる一種のglvalue(IDを持つもの)があり、それがxvalueです。 このトピックの中で後ほど、値カテゴリの全体像を見る際に、その考え方をもう一度取り上げます。

rvalue 参照と参照バインド ルール

このセクションでは、rvalue を参照するときの構文について説明します。 移動および転送の処理の多くについては別のトピックで説明されていますが、それらの問題の解決に必要なものは rvalue 参照です。 ただし、rvalue 参照について説明する前にまず、T& についてはっきりさせておく必要があります。つまり、これまで単に "参照" と呼んでいた事柄です。 それは本当は "lvalue (非定数) の参照" であり、参照のユーザーが書き込むことのできる値を参照しています。

template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.

左辺値参照は左辺値にバインドできますが、右辺値にはバインドできません。

それから、lvalue の const 参照が (T const&) があり、これは参照のユーザーが書き込むことの "できない" オブジェクトを参照します (たとえば、定数)。

template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.

lvalue の const 参照は、lvalue または rvalue にバインドできます。

T 型の rvalue 参照の構文は、T&& と記述します。 rvalue 参照では、移動可能な値、つまり使用後に内容を保持する必要がない値が参照されます (たとえば、一時的な値)。 そもそもの趣旨は、右辺値参照に束縛された値をムーブすること(その結果、その値を変更すること)にあるため、const および volatile 修飾(cv 修飾とも呼ばれます)は右辺値参照には適用されません。

template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.

右辺値参照は右辺値に束縛される。 実際、オーバーロードの解決では、rvalue は lvalue の const 参照より rvalue 参照にバインドする方を "優先されます"。 ただし、右辺値参照は左辺値にバインドできません。右辺値参照は、内容を保持する必要がないと想定されている値 (移動コンストラクターのパラメーターなど) を参照するためです。

また、コピー構造 (または、rvalue が xvalue の場合は移動構造) を介して、値渡しの引数が期待される rvalue を渡すこともできます。

glvalue には識別性がある。一方、prvalue にはない。

この段階では、ID を持つものがわかっています。 また、移動可能なものと動かないものを知っています。 しかし、ID を持 たない 値のセットにはまだ名前を付けていません。 そのセットは、 prvalue または 純粋右辺値と呼ばれます。

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}

glvalue には ID があります。prvalue が

値カテゴリの全体像

上記の情報と図を 1 つの大きな画像にまとめるだけです。

値カテゴリの全体像

glvalue (i)

glvalue (一般化された lvalue) は、ID を持っています。 「識別子を持つ」の略として「i」を使用します。

lvalue (i&!m)

lvalue (glvalue の一種) は ID を持っていますが、移動することはできません。 これらは、通常は読み取り/書き込みの値であり、参照、const 参照、またはコピーが低コストの場合は値によって、渡されます。 lvalue を rvalue 参照にバインドすることはできません。

xvalue (i&m)

xvalue は、glvalue の一種であると同時に rvalue の一種でもあり、同一性を持ち、かつムーブ可能です。 これは、コピーが高コストであるために移動することにしたかつての lvalue である場合があり、後でアクセスしないように注意する必要があります。 lvalue を xvalue に変換する方法を次に示します。

struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

上記のコード例では、まだ何も移動していません。 lvalue を名前のない rvalue 参照にキャストして xvalue を作成しただけです。 まだ lvalue の名前で識別できますが、xvalue として移動 "できる" ようになっています。 それを移動する理由と、実際の移動がどのようなものかについては、別の話題で扱うことにします。 ただし、"xvalue" の "x" は、それが役立つ場合は "expert-only" を意味すると考えることができます。 lvalue を xvalue (rvalue の一種) にキャストすることによって、値は rvalue 参照にバインドできるようになります。

xvalue の他の 2 つの例を次に示します。名前のない右辺値参照を返す関数を呼び出し、xvalue のメンバーにアクセスします。

struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

prvalue (!i&m)

prvalue(純粋な右辺値、右辺値の一種)には同一性はありませんが、移動可能です。 これらは通常、一時的なものか、値によって返される関数を呼び出した結果、または glvalue ではない他の式を評価した結果です。

rvalue (m)

右辺値は移動可能です。 "移動可能" の短縮形として "m" を使用します。

rvalue "参照" では、常に rvalue が参照されます (内容を保持する必要がないものと思われる値)。

ところで、rvalue 参照自体は rvalue なのでしょうか。 "名前のない" rvalue 参照 (上の xvalue コードの例にで示されているものなど) は xvalue なので、rvalue です。 移動コンストラクターなど、rvalue 参照関数パラメーターへのバインドが優先されます。 逆に (そして、おそらくは直感に反しますが)、rvalue 参照が名前を持っている場合、その名前で構成される式は lvalue です。 したがって、rvalue 参照パラメーターにバインドすることは "できません"。 ただし、名前のない rvalue 参照 (xvalue) に再びキャストするだけで、簡単にそのようにすることができます。

void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
    foo(a); // Calls foo(A&).
    foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.

!i&!m

ID を持っておらず、移動できない値の種類は、まだ説明していない 1 つの組み合わせです。 ただし、そのカテゴリは C++ 言語では役に立つアイデアではないので、無視できます。

参照縮小ルール

式に含まれる複数の似た参照 (lvalue 参照に対する lvalue 参照、または rvalue 参照に対する rvalue 参照) は、相互に相殺されます。

  • A& &A&に折りたたまれます。
  • A&& &&A&&に折りたたまれます。

式に含まれる複数の似ていない参照は、lvalue 参照に縮小されます。

  • A& &&A&に折りたたまれます。
  • A&& &A&に折りたたまれます。

参照の転送

この最後のセクションでは、既に説明した rvalue 参照と、別の概念である "転送参照" を比べます。 「転送参照」という用語が造語される前に、一部の人々は「ユニバーサルリファレンス」という用語を使用していました。

void foo(A&& a) { ... }
  • 前に見たように、A&& は rvalue 参照です。 const と volatile は、rvalue 参照には適用されません。
  • fooA 型の右辺値のみを受け入れます。
  • 右辺値参照 ( A&& など) が存在する理由は、一時的な (または他の右辺値) が渡される場合に最適化されたオーバーロードを作成できるようにするためです。
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&&転送参照です。 bar に何を渡すかによって、_Ty 型は、volatile/non-volatile とは独立に、const にも non-const にもなり得ます。
  • bar は、型 _Ty の任意の左辺値または右辺値を受け入れます。
  • lvalue を渡すと、転送参照は _Ty& && になり、lvalue 参照 _Ty& に縮小されます。
  • 右辺値を渡すと、転送参照が右辺値参照 _Ty&&になります。
  • 参照の転送 ( _Ty&&など) が存在する理由は、最適化 のためではなく 、渡した内容を受け取り、透過的かつ効率的に転送するためです。 転送参照が発生するのは、ライブラリ コード (コンストラクター引数を転送するファクトリ関数など) を記述 (または厳密に調査) した場合にのみ発生する可能性があります。

Sources

  • [Stroustrup, 2013] B. Stroustrup: C++ プログラミング言語、第 4 版。 Addison-Wesley。 2013.