1

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.

CPPL
  • 726
  • 1
  • 10
  • Did you tested your code? If yes, can you maybe share a link where we can see those errors. The error you mentioned are different from the error we get when trying to compiling the program. [Demo](https://onlinegdb.com/lr8v6D56c). Please provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) that produces the error you described. – Jason May 28 '22 at 04:43
  • The problem is that since you have defined a move constructor, the copy constructor will be implicitly deleted and hence it cannot be used when returning `*this` and `x` from `f2`. – Jason May 28 '22 at 04:53
  • @AnoopRana Thank you for your comments. I know that in my case the copy constructor is deleted. I was writing the code to kinda force the compiler to consider the move constructor (it may be a poor example...), and the reason it cannot use the move constructor seems to be related to the rvalue/lvalue property of the expression returned. My question is how the rvalue/lvalue property of the expression returned is determined? – CPPL May 28 '22 at 05:05

3 Answers3

1

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;.

Jason
  • 36,170
  • 5
  • 26
  • 60
  • Thank you for your detailed answer :) May I further ask if we have a function `f4` which `return x1+x2` (assume `operator+` is defined). I know that, after C++17, RVO will always be preformed so that the resulting value of `x1+x2` will be constructed directly into what is returned by `f4`. But let's say if there is no RVO, will `x1+x2` in the `return` statement be treated as rvalue or lvalue? – CPPL May 28 '22 at 12:27
  • @CPPL Note, `x1+x2` will already be an rvalue(assuming your overloaded `operator+` returns by value) and thus the compiler will directly use the move constructor.This will be different from your current question because in your current question you are first creating a local variable by writing `X x;` which implies `x` is an lvalue and then you're returning that lvalue expression by writing `return x`.But when you write `return x1+x2;`, the expression `x1+x2` will already be an rvalue and thus move ctor can be used and the quoted statement isn't applicable since `x1+x2` is not a local variable – Jason May 28 '22 at 12:32
  • So... can I say we only need to consider class.copy.elision#3 when we're returning an lvalue expression? – CPPL May 28 '22 at 12:37
  • @CPPL Not any lvalue expression. but only when we either return a local variable like `x` or a parameter of that function. Note this is mentioned in the first two lines of the bullet point that i quoted as : *"return statement is a id-expression that names an object with automatic storage duration declared in the body **or** parameter-declaration-clause"*. This is why the first case `f1` didn't work even though `*this` is an lvalue expression it was not a local object. – Jason May 28 '22 at 12:49
  • Thank you for your patience, two last questions... 1. if we are returning an unnamed object, say, `return X();`, then will `X()` be qualified to be treated as rvalue and thus call move ctor (if there's no RVO)? 2. if we are returning a reference to a local object (say, `X f5(){X x; X& r = x; return r;}`), then move ctor is not used. Is that because "reference is not object"? – CPPL May 28 '22 at 13:20
  • @CPPL **1)** In case you write `return X();` then yes, `X()` is already an rvalue just like `x1+x2` and hence if you're using C++14 or C++11 then RVO may apply.In C++17 though there is mandatory copy elison which means no move/copy ctor will be used in `return X();` andthe object will be constructed directly into storage where it otherwise would've been copied/moved to. **2)** We should never return a reference to a local variable because the local object will be destroyed once the function finishes.Also,yes reference in itself is not an object but an alias for another already existing object. – Jason May 28 '22 at 13:29
  • Oh I thought `X()` is an lvalue since we can do something like `X x; X()=x;` given there is copy assignment (although it's not useful at all) – CPPL May 28 '22 at 13:39
  • @CPPL No, `X()` is an rvalue. The reason we can do `X() = x;` is because it is allowed to assign to a class rvalue. See [here](https://stackoverflow.com/a/72315075/12002570) i have explained in some more detail how we can prevent this. Basically by using the `&` qualifier on the assignment operator. Also, feel free to ask a separate question(s) for your follow up questions so that the comment section don't get cluttered up and also here in the comment section there is word limit so i cannot express fully my answer and also these comments might get deleted by moderators in the future. – Jason May 28 '22 at 13:43
  • Thank you very much! I've learned a lot from you :) I'll organize my thoughts and see if there are other questions I'd like to post in the future. – CPPL May 28 '22 at 13:51
  • Hi, there is a [follow up question](https://stackoverflow.com/q/72421276/17172007) I came up with. You're welcome to have a look whenever you have time :) – CPPL May 29 '22 at 05:58
0

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]

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
-1

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;
Staz
  • 348
  • 1
  • 2
  • 6