Home > Enterprise >  The lvalue/rvalue property of expression in return statement
The lvalue/rvalue property of expression in return statement

Time:05-28

Consider the following toy code:

class X
{
public:
    X() { }
    X(X&& x) { }
    
    X f1()
    {
        return *this;  // error: rvalue reference to type X cannot bind to lvalue of type X
                       //                                                  ^^^^^^
    }
};

X f2()
{
    static X x;
    return x;  // error: rvalue reference to type X cannot bind to lvalue of type X
               //                                                  ^^^^^^
}

X f3()
{
    X x;  // in my understanding, the name x defined here is lvalue since variable expressions are lvalues
    return x;  // this is OK, hence I think x in return statement becomes rvalue
}

From the toy code shown above, I found that the expression in return statement used to construct the temporary returned by a function "remains" to be treated as lvalue if it is a reference or a static local variable while the expression "changes" to rvalue if it is a local variable. Apology for my understanding to be poor and flawed since I'm really new to concepts related to rvalue/lvalue in C ...

My question: generally speaking, what are the rules for expression in return statement to be rvalue/lvalue? How exactly is the lvalue/rvalue property of an expression "changes" when it is put in a return statement?


The error messages above are given by Resharper C when it tries to see if the move constructor can be applied. The aim for me to write the above code was just to see whether the expression in return statement is treated as rvalue or as lvalue.

CodePudding user response:

Lets see on case by case basis what is happening. The behavior of your program can be understood using class.copy.elision#3 which states:

In the following copy-initialization contexts, a move operation might be used instead of a copy operation:

  • If the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or

overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.

(emphasis mine)

Now we can use the above quoted statement to understand the behavior your given examples.

Case 1

Here we consider:

class X
{
public:
    X() { }
    X(X&& x) { }
    
    X f1()
    {
        return *this;
    }
};

In this case, the return expression is *this which is not an object with automatic storage duration declared in the body or parameter-declaration-clause of the function f1 and hence according to the above quoted statement here for the return statement the move operation might not be used in place of the copy operation.

This means that here the copy operation will be used but since the copy constructor for your class X is implicitly deleted we get the mentioned error saying:

error: use of deleted function ‘constexpr X::X(const X&)’
   14 |         return *this;

Case 2

Here we consider:

X f2()
{
    static X x;
    return x; 
}

In this case the variable x is a static local variable meaning it has static storage duration and not automatic storage duration. Hence according to the above quoted statement, here for the return statement the move operation might not be used in place of the copy operation.

This means that here the copy operation will be used but since the copy constructor for your class X is implicitly deleted we get the mentioned error saying:

error: use of deleted function ‘constexpr X::X(const X&)’
   21 |     return x;

Case 3

Here we consider:

X f3()
{
    X x; 
    return x; 
}

In this case however, x is a local variable with automatic storage duration and so the above quoted statement is applicable which means that the move operation can be used in place of the copy operation.

This is why we don't get any error in this case as you've provided the move constructor for your class X which can be used for the return statement return x;.

CodePudding user response:

This happens when the operand of return is an automatic variable name, which is not an lvalue reference, and not [a reference to] volatile.

This is described by [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.

[Note 3: This two-stage overload resolution is performed regardless of whether copy elision will occur. It determines the constructor or the return_­value overload to be called if elision is not performed, and the selected constructor or return_­value overload must be accessible even if the call is elided. — end note]

CodePudding user response:

The copy constructor is implicitly deleted because you defined that move constructor. Both f1 and f2 error because they're trying to call that deleted copy constructor. f3 is fine because NRVO is applicable. If the compiler decides not do apply NRVO for some reason, the move constructor will be called anyway.

So you'll have to define the copy contructor aswell.

X(const X &) = default;
  • Related