What Standard Clause Mandates This Lvalue-To-Rvalue Conversion

What standard clause mandates this lvalue-to-rvalue conversion?

I find it easier (if maybe not 100% precise) to think of lvalue-s as real objects and rvalue-s as the value stored in the object. The expression x is an lvalue expression that refers to the object x defined in the first line, but when used as the right hand side of an assignment to a type that is not a user defined type the actual value is read, and that is where the conversion from lvalue to rvalue is performed: reading the contents of the object.

As to the specific clause in the standard that dictates that conversion... well, the closest that I can think is 4.1 [conv.lvalue]/2 (Lvalue to Rvalue conversion):

The value contained in the object indicated by the lvalue is the rvalue result.

The requirement that the right hand side of the assignment is an rvalue is either implicit or missing from 5.17 [expr.ass], but that is the case or else the following expression would be an error since the rhs is an rvalue and there is no rvalue-to-lvalue conversion:

int x = 5;

EDIT: For initialization, 8.5 [dcl.init]/14, last bullet (which refers to fundamental types) states (emphasis mine):

  • Otherwise, the initial value of the object being initialized is the (possibly converted) value of the initializer expression. [...]

That value there means that the lvalue expression in your example is read (i.e. converted to an rvalue). At any rate the previous paragraph that referred to assignment could be applied here: if initialization required an lvalue rather than an rvalue, the expression int i = 0; would be ill-formed.

Regarding lvalue-to-rvalue conversion, when is it required?

So, this is generally one of those inferred and ill-specified parts of the standard; However, in 3.10

[ Note: some built-in operators expect lvalue operands. [ Example: built-in assignment operators all expect their left-hand operands to be lvalues. — end example ] Other built-in operators yield rvalues, and some expect them. [ Example: the unary and binary + operators expect rvalue arguments and yield rvalue results. — end example ] The discussion of each built-in operator in clause 5 indicates whether it expects lvalue operands and whether it yields an lvalue. — end note ]

Notice the poorly specified language "in clause 5 indicates whether it expects lvalue operands and whether it yields an lvalue".

Examination of Chapter 5 indicates that every case where an expression needs or returns an lvalue is enumerated, however very few cases dealing specifically with rvalues are enumerated, I think it's then assumed that the rest is assumed to be rvalues.

I also suspect it's poorly specified because from the perspective of the standard, it's not particularly important if the conversion is done implicitly or explicitly by the operator, regardless, the behavior should be the consistent and well-behaved.

lvalue to rvalue implicit conversion

I think the lvalue-to-rvalue conversion is more than just use an lvalue where an rvalue is required. It can create a copy of a class, and always yields a value, not an object.

I'm using n3485 for "C++11" and n1256 for "C99".


Objects and values

The most concise description is in C99/3.14:

object

region of data storage in the execution environment, the contents of which can represent
values

There's also a bit in C++11/[intro.object]/1

Some objects are polymorphic; the implementation generates information associated with
each such object that makes it possible to determine that object’s type during program execution. For other objects, the interpretation of the values found therein is determined by the type of the expressions used to access them.

So an object contains a value (can contain).


Value categories

Despite its name, value categories classify expressions, not values. lvalue-expressions even cannot be considered values.

The full taxonomy / categorization can be found in [basic.lval]; here's a StackOverflow discussion.

Here are the parts about objects:

  • An lvalue ([...]) designates a function or an object. [...]
  • An xvalue (an “eXpiring” value) also refers to an object [...]
  • A glvalue (“generalized” lvalue) is an lvalue or an xvalue.
  • An rvalue ([...]) is an xvalue, a temporary object or subobject thereof, or a value that is not associated with an object.
  • A prvalue (“pure” rvalue) is an rvalue that is not an xvalue. [...]

Note the phrase "a value that is not associated with an object". Also note that as xvalue-expressions refer to objects, true values must always occur as prvalue-expressions.


The lvalue-to-rvalue conversion

As footnote 53 indicates, it should now be called "glvalue-to-prvalue conversion". First, here's the quote:

1    A glvalue of a non-function, non-array type T can be converted to a prvalue. If T is an incomplete type, a program that necessitates this conversion is ill-formed. If the object to which the glvalue refers is not an object of type T and is not an object of a type derived from T, or if the object is uninitialized, a program
that necessitates this conversion has undefined behavior. If T is a non-class type, the type of the prvalue is the cv-unqualified version of T. Otherwise, the type of the prvalue is T.

This first paragraph specifies the requirements and the resulting type of the conversion. It isn't yet concerned with the effects of the conversion (other than Undefined Behaviour).

2    When an lvalue-to-rvalue conversion occurs in an unevaluated operand or a subexpression thereof the value contained in the referenced object is not accessed. Otherwise, if the glvalue 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, if the glvalue has (possibly cv-qualified) type std::nullptr_t, the
prvalue result is a null pointer constant. Otherwise, the value contained in the object indicated by the glvalue is the prvalue result.

I'd argue that you'll see the lvalue-to-rvalue conversion most often applied to non-class types. For example,

struct my_class { int m; };

my_class x{42};
my_class y{0};

x = y;

The expression x = y does not apply the lvalue-to-rvalue conversion to y (that would create a temporary my_class, by the way). The reason is that x = y is interpreted as x.operator=(y), which takes y per default by reference, not by value (for reference binding, see below; it cannot bind an rvalue, as that would be a temporary object different from y). However, the default definition of my_class::operator= does apply the lvalue-to-rvalue conversion to x.m.

Therefore, the most important part to me seems to be

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

So typically, an lvalue-to-rvalue conversion will just read the value from an object. It isn't just a no-op conversion between value (expression) categories; it can even create a temporary by calling a copy constructor. And the lvalue-to-rvalue conversion always returns a prvalue value, not a (temporary) object.

Note that the lvalue-to-rvalue conversion is not the only conversion that converts an lvalue to a prvalue: There's also the array-to-pointer conversion and the function-to-pointer conversion.


values and expressions

Most expressions don't yield objects[[citation needed]]. However, an id-expression can be an identifier, which denotes an entity. An object is an entity, so there are expressions which yield objects:

int x;
x = 5;

The left hand side of the assignment-expression x = 5 also needs to be an expression. x here is an id-expression, because x is an identifier. The result of this id-expression is the object denoted by x.

Expressions apply implicit conversions: [expr]/9

Whenever a glvalue expression appears as an operand of an operator that expects a prvalue for that operand, the lvalue-to-rvalue, array-to-pointer, or function-to-pointer standard conversions are applied to convert the expression to a prvalue.

And /10 about usual arithmetic conversions as well as /3 about user-defined conversions.

I'd love now to quote an operator that "expects a prvalue for that operand", but cannot find any but casts. For example, [expr.dynamic.cast]/2 "If T is a pointer type, v [the operand] shall be a prvalue of a pointer to complete class type".

The usual arithmetic conversions required by many arithmetic operators do invoke an lvalue-to-rvalue conversion indirectly via the standard conversion used. All standard conversions but the three that convert from lvalues to rvalues expect prvalues.

The simple assignment however doesn't invoke the usual arithmetic conversions. It is defined in [expr.ass]/2 as:

In simple assignment (=), the value of the expression replaces that of the object referred to by the left operand.

So although it doesn't explicitly require a prvalue expression on the right hand side, it does require a value. It is not clear to me if this strictly requires the lvalue-to-rvalue conversion. There's an argument that accessing the value of an uninitialized variable should always invoke undefined behaviour (also see CWG 616), no matter if it's by assigning its value to an object or by adding its value to another value. But this undefined behaviour is only required for an lvalue-to-rvalue conversion (AFAIK), which then should be the only way to access the value stored in an object.

If this more conceptual view is valid, that we need the lvalue-to-rvalue conversion to access the value inside an object, then it'd be much easier to understand where it is (and needs to be) applied.


Initialization

As with simple assignment, there's a discussion whether or not the lvalue-to-rvalue conversion is required to initialize another object:

int x = 42; // initializer is a non-string literal -> prvalue
int y = x; // initializer is an object / lvalue

For fundamental types, [dcl.init]/17 last bullet point says:

Otherwise, the initial value of the object being initialized is the (possibly converted) value of the initializer expression. Standard conversions will be used, if necessary, to convert the initializer expression to the cv-unqualified version of the destination type; no user-defined conversions are considered. If the conversion cannot be done, the initialization is ill-formed.

However, it also mentioned the value of the initializer expression. Similar to the simple-assignment-expression, we can take this as an indirect invocation of the lvalue-to-rvalue conversion.


Reference binding

If we see lvalue-to-rvalue conversion as a way to access the value of an object (plus the creation of a temporary for class type operands), we understand that it's not applied generally for binding to a reference: A reference is an lvalue, it always refers to an object. So if we bound values to references, we'd need to create temporary objects holding those values. And this is indeed the case if the initializer-expression of a reference is a prvalue (which is a value or a temporary object):

int const& lr = 42; // create a temporary object, bind it to `r`
int&& rv = 42; // same

Binding a prvalue to an lvalue reference is prohibited, but prvalues of class types with conversion functions that yield lvalue references may be bound to lvalue references of the converted type.

The complete description of reference binding in [dcl.init.ref] is rather long and rather off-topic. I think the essence of it relating to this question is that references refer to objects, therefore no glvalue-to-prvalue (object-to-value) conversion.

Does lvalue-to-rvalue conversion ever happen to class types?

An example is volatile objects in discarded-value expressions:

struct A {};

void f()
{
volatile A a;
a;
}

According to [expr.context]/2:

In some contexts, an expression only appears for its side effects.
Such an expression is called a discarded-value expression. The
array-to-pointer and function-to-pointer standard conversions are not
applied. The lvalue-to-rvalue conversion is applied if and only if
the expression is a glvalue of volatile-qualified type and it is one
of the following
:

  • ...
  • id-expression,
  • ...

Lvalue-to-rvalue conversion is applied to a.

Lvalue to Rvalue conversions

Yes. Without lvalue-to-rvalue conversion, it cannot read it's value.

Loosely speaking, think of lvalue as some sort of container, and rvalue as the value contained in the container.

C++03, section §3.10/7 reads,

Whenever an lvalue appears in a context where an rvalue is expected, the lvalue is converted to an rvalue; see 4.1, 4.2, and 4.3.

Read it along with, §4.1/2 (your quotation),

The value contained in the object indicated by the lvalue is the rvalue result.

Related topic:

  • What standard clause mandates this lvalue-to-rvalue conversion?

Lvalue-to-rvalue conversion in [expr.ref]/2

Bear in mind that prvalues can be converted to xvalues via a temporary materialization conversion [conv.rval]:

A prvalue of type T can be converted to an xvalue of type T. This conversion initializes a temporary object (15.2) of type T from the prvalue by evaluating the prvalue with the temporary object as its result
object, and produces an xvalue denoting the temporary object. T shall be a complete type. [Note: If T is a class type (or array thereof), it must have an accessible and non-deleted destructor; see 15.4. — end note] [Example:

struct X { int n; };
int k = X().n;
// OK, X() prvalue is converted to xvalue

end example]

Prior to the introduction of this new prvalue-to-glvalue conversion, C++14 did not have the restriction for the postfix-expression to be a glvalue.

On that note, C++11 was the first revision to feature user-definable, unconstrained rvalue-to-lvalue conversions by means of the (then-)new rvalue reference type: auto&& x = f(); makes the prvalue f() into an xvalue x.

Standard reference for int foo = foo

3.3.1 Point of declaration [basic.scope.pdecl]

The point of declaration for a name is immediately after its complete declarator (clause 8) and before its initializer (if any),

The behaviour is well defined if the declaration is at file scope. If you have the declaration at function scope and if you use foo later on [which would be initialized to some unspecified value in that case] the behaviour would be undefined.



Related Topics



Leave a reply



Submit