In C++, What Categories (Lvalue, Rvalue, Xvalue, etc.) Can Expressions That Produce Temporaries of Class Type Fall Into

In C++, what categories (lvalue, rvalue, xvalue, etc.) can expressions that produce temporaries of class type fall into?

Every expression is one, and only one, of:

  • lvalue
  • xvalue
  • prvalue

The union of expressions that are lvalues and xvalues are known collectively as glvalues.

The union of expressions that are xvalues and prvalues are known collectively as rvalues.

Thus xvalue expressions are known both as glvalues and rvalues.

The handy diagram found in Alf's answer correctly illustrates the relationship I've described with words above, and is also found in section 3.10 of the C++ standards, versions C++11 and above.

Everything I've said above, I suspect the OP already knew, just from the wording of the title of this question.


Trivia:

Bjarne Stroustrup invented this classification of expressions, and in
doing so perhaps saved the entire rvalue reference proposal from
collapsing in the Core Working Group. I will be forever grateful.


What I'm adding is a way to discover for yourself which of the three bottom classification categories any expression falls into: lvalue, xvalue or prvalue.

#include <type_traits>
#include <typeinfo>
#include <iostream>
#ifndef _MSC_VER
# include <cxxabi.h>
#endif
#include <memory>
#include <string>
#include <cstdlib>

template <typename T>
std::string
expression_name()
{
typedef typename std::remove_reference<T>::type TR;
std::unique_ptr<char, void(*)(void*)> own
(
#ifndef _MSC_VER
__cxxabiv1::__cxa_demangle(typeid(TR).name(), nullptr,
nullptr, nullptr),
#else
nullptr,
#endif
std::free
);
std::string r = own != nullptr ? own.get() : typeid(TR).name();
if (std::is_const<TR>::value)
r += "const ";
if (std::is_volatile<TR>::value)
r += "volatile ";
if (std::is_lvalue_reference<T>::value)
r = "lvalue expression of type " + r;
else if (std::is_rvalue_reference<T>::value)
r = "xvalue expression of type " + r;
else
r = "prvalue expression of type " + r;
return r;
}

The above function can be used like:

std::cout << "some_expression is a " << expression_name<decltype(some_expression)>() << '\n';

And it will answer this OP's question. For example:

int main()
{
std::cout << "Foo(5) is a " << expression_name<decltype(Foo(5))>() << '\n';
std::cout << "Foo(5).get_addr() is a " << expression_name<decltype(Foo(5).get_addr())>() << '\n';
std::cout << "Foo(78) = Foo(86) is a " << expression_name<decltype(Foo(78) = Foo(86))>() << '\n';
std::cout << "++Foo(5) is a " << expression_name<decltype(++Foo(5))>() << '\n';
std::cout << "++(Foo(78) + Foo(86)) is a " << expression_name<decltype(++(Foo(78) + Foo(86)))>() << '\n';
std::cout << "Foo(78) + Foo(86) is a " << expression_name<decltype(Foo(78) + Foo(86))>() << '\n';
std::cout << "std::move(Foo(5)) is a " << expression_name<decltype(std::move(Foo(5)))>() << '\n';
std::cout << "std::move(++Foo(5)) is a " << expression_name<decltype(std::move(++Foo(5)))>() << '\n';
}

For me prints out:

Foo(5) is a prvalue expression of type Foo
Foo(5).get_addr() is a prvalue expression of type void*
Foo(78) = Foo(86) is a lvalue expression of type Foo
++Foo(5) is a lvalue expression of type Foo
++(Foo(78) + Foo(86)) is a lvalue expression of type Foo
Foo(78) + Foo(86) is a prvalue expression of type Foo
std::move(Foo(5)) is a xvalue expression of type Foo
std::move(++Foo(5)) is a xvalue expression of type Foo

One area to be careful of in the use of this function:

decltype(variable_name) will give the declared type of the variable name. If you want to discover the value category of the expression when variable_name is used (as opposed to its declared type), then you need to add extra parentheses around (variable_name) when used in decltype. That is:

decltype((variable_name))

is the type of the expression variable_name, and not the declared type of variable_name.

For example given:

    Foo&& foo = Foo(5);
std::cout << "foo is a " << expression_name<decltype(foo)>() << '\n';

This will erroneously output:

foo is a xvalue expression of type Foo

Add the extra parentheses to the decltype:

    std::cout << "foo is a " << expression_name<decltype((foo))>() << '\n';

to convert foo from a type name into an expression. Now the output is:

foo is a lvalue expression of type Foo

If you are unsure whether you need to add parentheses or not to get the correct answer, then just add them. Adding them won't make a correct answer wrong -- unless you are looking to get the declared type of a variable, and not the type of an expression. And in that latter case, you want a closely related function: type_name<T>().

Empirically determine value category of C++11 expression?

decltype can return the declared type of an entity (hence the name), but can also be used to query the type of an expression. However, in the latter case the resulting type is 'adjusted' according to the value category of that expression: an lvalue expression results in an lvalue reference type, an xvalue in an rvalue reference type, and a prvalue in just the type. We can use this to our benefit:

template<typename T>
struct value_category {
// Or can be an integral or enum value
static constexpr auto value = "prvalue";
};

template<typename T>
struct value_category<T&> {
static constexpr auto value = "lvalue";
};

template<typename T>
struct value_category<T&&> {
static constexpr auto value = "xvalue";
};

// Double parens for ensuring we inspect an expression,
// not an entity
#define VALUE_CATEGORY(expr) value_category<decltype((expr))>::value

Rvalue to lvalue conversion and named-refs-are-lvalues rule

arg the variable has type int&& and no value category.

arg the expression (it is an expression at lines 3 and 4) has type int and value category "lvalue"

Lvalue to rvalue conversion changes the value category of an expression, without changing its type. If you write arg+1 inside the function, the lvalue expression arg of type int would undergo this conversion to produce a prvalue expression of type int, since that's what built-in + requires.

There are no "lvalue to rvalue" or reverse conversions between int& and int&& because expressions never have reference types. The error in your program is a failure to bind an rvalue reference (of type int&&) to an lvalue expression (of type int).

xvalues: differences between non class types and class types


I'm trying to reply to my own question with a bunch of links to the standard.

I'm quite sure that I'll write something that is terribly wrong and someone will come banding words with me.

Well, I did my best to explain how one can deduce from the standard what's described in the question.

Feel free to downvote if needed, but please let me know what's wrong so as to be able to fix the answer and understand the error.

Thank you.


3.9/8 (types):

An object type is a (possibly cv-qualified) type that is not a function type, not a reference type, and not cv void.

5.2.2/10 (expressions, function call):

A function call is [...] an xvalue if the result type is an rvalue reference to object type

Thus std::move is an xvalue expression in either cases.

5.18/3 (assignment):

If the left operand is not of class type, the expression is implicitly converted [...] to the cv-unqualified type of the left operand.

This does not add useful information, but it's for the sake of completeness.


4.1/2 (lvalue-to-rvalue conversion):

Otherwise, if T has a class type, the conversion copy-initializes a temporary of type T from the glvalue and the result of the conversion is a prvalue for the temporary.

Otherwise, the value contained in the object indicated by the glvalue is the prvalue result.

12.2 (temporary objects) does the rest.

So, as mentioned by @xaxxon in the comments, I was actually trying to do (let me write) 42 = 0; and it is not a valid expression in C++.

As correctly pointed out in the comments by @bogdan, the right part of the standard to which to refer in this case is 5.18/1 (Assignment):

All require a modifiable lvalue as their left operand [...]

While 5/2 and 5/3 clarify that the statement applies to built-in operators only.

Rvalues, lvalues and formal definitions

Some preliminary paragraphs first:

[basic]

3 An entity is a value, object, reference, function, enumerator,
type, class member, template, template specialization, namespace,
parameter pack, or this.

[dcl.type.simple]

4 The type denoted by decltype(e) is defined as follows:

  • if e is an unparenthesized id-expression or an unparenthesized class member access ([expr.ref]), decltype(e) is the type of the
    entity named by e. If there is no such entity, or if e names a set
    of overloaded functions, the program is ill-formed;

  • otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e;

  • otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;

  • otherwise, decltype(e) is the type of e.


[dcl.ref]

1 In a declaration T D where D has either of the forms


& attribute-specifier-seqopt D1
&& attribute-specifier-seqopt D1

and the type of the identifier in the declaration T D1 is
derived-declarator-type-list T,” then the type of the identifier
of D is “derived-declarator-type-list reference to T.”

[expr]

5 If an expression initially has the type “reference to T
([dcl.ref], [dcl.init.ref]), the type is adjusted to T prior to any
further analysis. The expression designates the object or function
denoted by the reference, and the expression is an lvalue or an
xvalue, depending on the expression.

[expr.prim.general]

8 An identifier is an id-expression provided it has been suitably
declared (Clause [dcl.dcl]). The type of the expression is the type of
the identifier. The result is the entity denoted by the identifier.
The result is an lvalue if the entity is a function, variable, or data
member and a prvalue otherwise.

[expr.call]

10 A function call is an lvalue if the result type is an lvalue
reference type or an rvalue reference to function type, an xvalue if
the result type is an rvalue reference to object type, and a prvalue
otherwise.

Which now allows us to answer your questions.

In the line 1, x is an identifier (id-expression) that names a function parameter. Its type is int&&, and this is the type that decltype(x) returns. x is not an expression and has no value category.

Yes of sorts. x in the declaration is not an expression. But as an argument to decltype is an expression. However, it hits the special case of decltype's first bullet so the type of the identifier named by x is deduced, instead of the type of x as an expression.

In the line 2, x is an expression. Before type adjustment its type is int&&, and after the type becomes int. The value category is lvalue.

Yes.

In the line 3, std::move(x) is an expression. Its type before adjustment is int&&, after - int. The value category is xvalue.

Yes.

When we say that x has rvalue reference type, we refer either to the type of x as an identifier, or to the type of x as an expression before type adjustment.

Yes.

The word "type" in the statement "Each expression has some non-reference type, and each expression belongs to exactly one of the three primary value categories" at cppreference.com refers to the type after type adjustment.

Yes.

When Scott Meyers writes "If the type of an expression is an lvalue reference (e.g., T& or const T&, etc.), that expression is an lvalue." he refers to the type before adjustment, and the second word "lvalue" refers to the value category.

Can't really say for sure what Scott Meyers meant when he wrote this, but that is the only interpretation of the words that matches up with the standard, yes.



Related Topics



Leave a reply



Submit