Is a variable that is about to go out of scope an lvalue or an xvalue?
Asked Answered
S

2

5

When a variable that is about to go out of scope is returned or thrown, its resources can be reused i.e. it can be moved from, as the following C++ program shows:

#include <iostream>

struct X {
    X() {
        std::cout << "X()\n";
    }
    
    X(X&&) {
        std::cout << "X(X&&)\n";
    }
    
    ~X() {
        std::cout << "~X()\n";
    }
};

X f() {
    X x;
    std::cout << "return\n";
    return x;
}

int main() {
    try {
        X x = f();
        std::cout << "throw\n";
        throw x;
    } catch (...) {
        std::cout << "catch\n";
    }
    return 0;
}

Compilation (we turn off copy/move elision with the flag -fno-elide-constructors), linkage, and execution:

clang++ -std=c++17 -O2 -Wall -pedantic -pthread -fno-elide-constructors\
main.cpp && ./a.out

Output:

X()
return
X(X&&)
~X()
throw
X(X&&)
~X()
catch
~X()

x in the above statements return x; and throw x; denotes an object whose resources can be reused.

In the Working Draft, Standard for Programming Language C++, [basic.lval-1], we have the following definitions for value categories:

  • A glvalue is an expression whose evaluation determines the identity of an object or function.
  • An xvalue is a glvalue that denotes an object whose resources can be reused (usually because it is near the end of its lifetime).
  • An lvalue is a glvalue that is not an xvalue.

So is x an lvalue or an xvalue?

Slide answered 8/9, 2021 at 8:24 Comment(0)
L
6

x is an lvalue itself.

The following expressions are lvalue expressions:

  • the name of a variable, a function, a template parameter object (since C++20), or a data member, regardless of type, such as std::cin or std::endl. Even if the variable's type is rvalue reference, the expression consisting of its name is an lvalue expression;

For local variables as x, in return statement and throw expression, overload resolution for initialization is performed in two-phases; firstly as if x is an rvalue expression (then move constructor might be selected).

In return statement:

(Since C++11) then overload resolution to select the constructor to use for initialization of the returned value or, for co_return, to select the overload of promise.return_value() (since C++20) is performed twice:

  • first as if expression were an rvalue expression (thus it may select the move constructor), and

In throw expression:

  • This may also call the move constructor for lvalue expressions if they name local variables or function or catch-clause parameters whose scope does not extend past the innermost enclosing try-block (if any), by same two-step overload resolution as in return statement (since C++17)

As the effect the move constructor is selected in both cases. This is just special for return and throw, doesn't mean x is rvalue or xvalue entirely. if you write sth like X x2(x); in f(), the copy constructor will be selected (and causes error because the copy constructor is implicitly-deleted).

From the standard, [class.copy.elision]/3:

An implicitly movable entity is a variable of automatic storage duration that is either a non-volatile object or an rvalue reference to a non-volatile object type. In the following copy-initialization contexts, a move operation is first considered before attempting a copy operation:

(3.1) - If the expression in a return ([stmt.return]) or co_­return ([stmt.return.coroutine]) statement is a (possibly parenthesized) id-expression that names an implicitly movable entity declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or

(3.2) - if the operand of a throw-expression ([expr.throw]) is a (possibly parenthesized) id-expression that names an implicitly movable entity that belongs to a scope that does not contain the compound-statement of the innermost try-block or function-try-block (if any) whose compound-statement or ctor-initializer contains the throw-expression,

overload resolution to select the constructor for the copy or the return_­value overload to call is first performed as if the expression or operand were an rvalue. If the first overload resolution fails or was not performed, overload resolution is performed again, considering the expression or operand as an lvalue.

[expr.prim.id.unqual]/3:

The expression is an lvalue if the entity is a function, variable, structured binding, data member, or template parameter object and a prvalue otherwise ([basic.lval]);

[basic.lval]:

(1.1) - A glvalue is an expression whose evaluation determines the identity of an object or function.

(1.2) - A prvalue is an expression whose evaluation initializes an object or computes the value of an operand of an operator, as specified by the context in which it appears, or an expression that has type cv void.

(1.3) - An xvalue is a glvalue that denotes an object whose resources can be reused (usually because it is near the end of its lifetime).

(1.4) - An lvalue is a glvalue that is not an xvalue.

(1.5) - An rvalue is a prvalue or an xvalue.

[Note 3: An expression is an xvalue if it is:

(4.1) - the result of calling a function, whether implicitly or explicitly, whose return type is an rvalue reference to object type ([expr.call]),

(4.2) - a cast to an rvalue reference to object type ([expr.type.conv], [expr.dynamic.cast], [expr.static.cast] [expr.reinterpret.cast], [expr.const.cast], [expr.cast]),

(4.3) - a subscripting operation with an xvalue array operand ([expr.sub]),

(4.4) - a class member access expression designating a non-static data member of non-reference type in which the object expression is an xvalue ([expr.ref]), or

(4.5) - a .* pointer-to-member expression in which the first operand is an xvalue and the second operand is a pointer to data member ([expr.mptr.oper]).

Lympho answered 8/9, 2021 at 8:35 Comment(7)
Thanks. Could you quote the C++ standard instead of a secondary source (cppreference is great but I would like an answer derived only from the primary source)?Brasilein
@Maggyero Answer revised.Lympho
Thanks. x is exactly that: ‘a (possibly parenthesized) id-expression that names an implicitly movable entity’. And [[expr.prim.id.unqual#3]](timsong-cpp.github.io/cppwp/expr.prim.id.unqual#3) specifies this about unqualified id-expressions: ‘The expression is an lvalue if the entity is a function, variable, structured binding, data member, or template parameter object and a prvalue otherwise ([basic.lval]); it is a bit-field if the identifier designates a bit-field.’ So x seems to be an lvalue. But x seems to also fit the definition of xvalues since its resources can be reused.Brasilein
@Maggyero It's just treated as if an rvalue expression in special return and throw cases. Since it's not listed in the definition of xvalue so it's not xvalue in strict sense. Yes I agree these descriptions are a little confusing.Lympho
Let’s ask the experts.Brasilein
The draft issue list is not to "ask the experts". It's to report editorial problems in the draft, not ask what something means.Hankhanke
@Lympho is entirely correct, it's an lvalue, but there are special cases where the resources of an lvalue can be reused, see [class.copy.elision]. The fact that an lvalue's resources can be reused in some specific circumstances doesn't mean it is an xvalue.Hankhanke
H
2

So is x an lvalue or an xvalue?

It's an lvalue.

[class.copy.elision] allows lvalues to be moved as though they were xvalues, but that doesn't mean they actually are xvalues. They can just be treated in a similar way to xvalues in specific contexts such as the operand of a return statement or a throw expression.

But x is still an lvalue in each of #1 #2 and #3 here:

    try {
        X x = f();   // #1
        auto x2 = x; // #2
        std::cout << "throw\n";
        throw x;     // #3
    } catch (...) {
        std::cout << "catch\n";
    }
Hankhanke answered 8/9, 2021 at 23:55 Comment(1)
Then what about adding this precision (in bold) to the definition of xvalues to make it correct? ‘An xvalue is a glvalue that denotes an object whose resources can always be reused (usually because it is near the end of its lifetime).’, that is in any contexts. This would also make the definition of lvalues correct: ‘An lvalue is a glvalue that is not an xvalue.’ since it would mean it is a glvalue not always movable from, allowing the special contexts of return and throw operands.Brasilein

© 2022 - 2024 — McMap. All rights reserved.