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

2

6

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.

Toothpick answered 19/7, 2017 at 1:21 Comment(4)
I used std::string for simplicity. What if it's a heavy object or non-COW string holding megabytes of data?Toothpick
@tadman copying a string is not cheap. It can be very expensive especially if it is large like the OP statesModern
@Modern Hm, I thought modern implementations of std::string used a copy-on-write tracking mechanism to make copies cheaper. In doing some testing here I am seeing performance degrade, though it's only measurable with strings over 16KB.Talbott
@Modern This answer sheds light on that so that's quite a surprise. Thanks for pointing that out!Talbott
H
8

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

Henton answered 19/7, 2017 at 1:56 Comment(4)
Are you familiar with 12.8/32? Does it not apply to the 2nd question?Toothpick
@innocent You seem to think thise questions are distinct. You are wrong.Weltschmerz
n4296Toothpick
@Yakk I believe they are 2 sides of the same coin, that's why I asked them in the same questionToothpick
K
-3

Why not just use outer for everything? Or if working with a function pass in &outer . Then, inside the function work with *inner.

Kinship answered 19/7, 2017 at 1:49 Comment(1)
Thank you for your suggestion. The above is an oversimplified example. If you don't have an answer to the question, please don't answer.Toothpick

© 2022 - 2024 — McMap. All rights reserved.