Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can an lvalue at end of scope be treated as an rvalue?

EDIT: Consider 2 following examples:

std::string x;
{
    std::string y = "extremely long text ...";
    ...
    x = y; // *** (1)
}
do_something_with(x);

struct Y
{
    Y();
    Y(const Y&);
    Y(Y&&);
    ... // many "heavy" members
};

struct X
{
    X(Y y) : y_(std::move(y)) { }
    Y y_;
}

X foo()
{
    Y y;
    ...
    return y; // *** (2)
}

In both examples y on lines (1) and (2) is near end of its lifetime and is about to be destroyed. It seems obvious that it can be treated as an rvalue and be moved in both cases. In (1) its contents can be moved into x and in (2) into temp instance of X().y_.

My questions are:

1) Will it be moved in either of the above examples? (a) If yes, under what standard provision. (b) If no, why not? Is that an omission in the standard or is there another reason that I am not thinking of?

2) If the above answer is NO. In the first example I can change (1) to x = std::move(y) to force the compiler to perform the move. What can I do in the second example to indicate to the compiler that y can be moved? return std::move(y)?

NB: I am purposely returning an instance of Y and not X in (2) to avoid (N)RVO.

like image 362
Innocent Bystander Avatar asked Jul 19 '17 01:07

Innocent Bystander


1 Answers

First Example

For your first example, the answer is clearly "no". The standard gives permission for the compiler to take various liberties about copies (even with side effects) when dealing with the return value from a function. I suppose, in the specific case of std::string, the compiler could "know" that neither copying nor moving has any side effects, so it could substitute one for the other under the as-if rule. If, however, we had something like:

struct foo {
    foo(foo const &f) { std::cout << "copy ctor\n"; }
    foo(foo &&f) { std::cout << "move ctor\n"; }
};

foo outer;
{ 
    foo inner;
    // ...
    outer = inner;
}

...a properly functioning compiler must generate code that prints out "copy ctor", not "move ctor". There's really no specific citation for this--rather, there are citations talking about exceptions for return values from functions, which don't apply here because we're not dealing with the return value from a function.

As to why nobody's dealt with this: I'd guess that it's simply because nobody's bothered. Returning values from functions happens often enough that it's worth putting a fair amount of effort into optimizing it. Creating a non-function block, and creating a value in the block that you proceed to copy to a value outside the block to maintain its visibility happens rarely enough that it seems unlikely anybody's written up a proposal.

Second Example

This example is at least returning a value from a function--so we have to look at the specifics of the exceptions that allow moves instead of copies.

Here, the rule is (N4659, §[class.copy.elision]/3):

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

  • If the expression in a return statement (9.6.3) 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,

[...]

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.

The expression (y) in your return statement is an id-expression that names an object with automatic storage duration declared in the body of the innermost enclosing function, so the compiler must do the two-stage overload resolution.

However, what it's looking for at this point is a constructor to create an X from a Y. X defines one (and only one) such constructor--but that constructor receives its Y by value. Since that's the only available constructor, that's the one that "wins" in overload resolution. Since it takes its argument by value, the fact that we first tried overload resolution treating y as an rvalue doesn't really make any difference, because X doesn't have a ctor of the right type to receive it.

Now, if we defined X something like this:

struct X
{
    X(Y &&y);
    X(Y const &y); 

    Y y_;
}

...then the two-stage overload resolution would have a real effect--even though y designates an lvalue, the first round of overload resolution treats it as an rvalue, so X(Y &&y) would be selected and used to create the temporary X that gets returned--that is, we'd get a move instead of a copy (even though y is an lvalue, and we have a copy constructor that takes an lvalue reference).

like image 188
Jerry Coffin Avatar answered Sep 30 '22 18:09

Jerry Coffin