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).
std::string
for simplicity. What if it's a heavy object or non-COW string holding megabytes of data? – Toothpickstd::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