Can Compiler Generate Std::Move for a Last Use of Lvalue Automatically

Can compiler generate std::move for a last use of lvalue automatically?

No. Consider:

using X = std::shared_ptr<int>;
void g(X);
void f() {
X b = std::make_shared<int>();
int &i = *b;
g(b); // last use of 'b'
i = 5;
}

In general, the compiler cannot assume that altering the semantics of copies, moves and destructors of X will be a legitimate change without performing analysis on all the code surrounding the use of b (i.e., the whole of f, g, and all the types used therein).

Indeed, in some cases whole-program analysis may be necessary:

using X = std::shared_ptr<std::lock_guard<std::mutex>>;
std::mutex i_mutex;
int i;
void g(X);
void f() {
X b = std::make_shared<std::lock_guard<std::mutex>>(i_mutex);
g(b); // last use of 'b'
i = 5;
}

If b is moved, this introduces a data race against other threads that synchronize access to i using i_mutex.

Do compilers automatically use move semantics when a movable object is used for the last time?

Is this optimization allowed by the C++0x Standard?

No.

Do the compilers employ it? Even in
complex cases, i.e. the function
consists from more than one line?

No.

How reliable is this optimization,
i.e. can I expect the compiler to
utilize it as much as I expect the
compiler to apply Return Value
Optimization?

You should decorate A(const A&) and A(A&&) with print statements and run test cases of interest to you. Don't forget to test lvalue arguments if those use cases are part of your design.

The correct answers will depend upon how expensive the copy and move of A are,how many arguments Object::value actually has, and how much code repetition you're willing to put up with.

Finally, be very suspicious of any guideline that contains words like "always" or "everywhere". E.g. I use goto every once in a while. But other programmers have words like "never" associated with goto. But every once in a while, you can't beat a goto for both speed and clarity.

There will be times you should favor a pair of foo(const A&) foo(A&&) over foo(A). And times you won't. Your experiments with decorated copy and move members will guide you.

Can an optimizing compiler add std::move?

The compiler is required to behave as-if the copy occurred from the vector to the call of Foo.

If the compiler can prove that there are is a valid abstract machine behavior with no observable side effects (within the abstract machine behavior, not in a real computer!) that involves moving the std::vector into Foo, it can do this.

In your above case, this (moving has no abstract machine visible side effects) is true; the compiler may not be able to prove it, however.

The possibly observable behavior when copying a std::vector<T> is:

  • Invoking copy constructors on the elements. Doing so with int cannot be observed
  • Invoking the default std::allocator<> at different times. This invokes ::new and ::delete (maybe1) In any case, ::new and ::delete has not been replaced in the above program, so you cannot observe this under the standard.
  • Calling the destructor of T more times on different objects. Not observable with int.
  • The vector being non-empty after the call to Foo. Nobody examines it, so it being empty is as-if it was not.
  • References or pointers or iterators to the elements of the exterior vector being different than those inside. No references, vectors or pointers are taken to the elements of the vector outside Foo.

While you may say "but what if the system is out of memory, and the vector is large, isn't that observable?":

The abstract machine does not have an "out of memory" condition, it simply has allocation sometimes failing (throwing std::bad_alloc) for non-constrained reasons. It not failing is a valid behavior of the abstract machine, and not failing by not allocating (actual) memory (on the actual computer) is also valid, so long as the non-existence of the memory has no observable side effects.

A slightly more toy case:

int main() {
int* x = new int[std::size_t(-1)];
delete[] x;
}

while this program clearly allocates way too much memory, the compiler is free to not allocate anything.

We can go further. Even:

int main() {
int* x = new int[std::size_t(-1)];
x[std::size_t(-2)] = 2;
std::cout << x[std::size_t(-2)] << '\n';
delete[] x;
}

can be turned into std::cout << 2 << '\n';. That large buffer must exist abstractly, but as long as your "real" program behaves as-if the abstract machine would, it doesn't actually have to allocate it.

Unfortunately, doing so at any reasonable scale is difficult. There are lots and lots of ways information can leak from a C++ program. So relying on such optimizations (even if they happen) is not going to end well.


1 There was some stuff about coalescing calls to new that might confuse the issue, I am uncertain if it would be legal to skip calls even if there was a replaced ::new.


An important fact is that there are situations that the compiler is not required to behave as-if there was a copy, even if std::move was not called.

When you return a local variable from a function in a line that looks like return X; and X is the identifier, and that local variable is of automatic storage duration (on the stack), the operation is implicitly a move, and the compiler (if it can) can elide the existence of the return value and the local variable into one object (and even omit the move).

The same is true when you construct an object from a temporary -- the operation is implicitly a move (as it is binding to an rvalue) and it can elide away the move completely.

In both these cases, the compiler is required to treat it as a move (not a copy), and it can elide the move.

std::vector<int> foo() {
std::vector<int> x = {1,2,3,4};
return x;
}

that x has no std::move, yet it is moved into the return value, and that operation can be elided (x and the return value can be turned into one object).

This:

std::vector<int> foo() {
std::vector<int> x = {1,2,3,4};
return std::move(x);
}

blocks elision, as does this:

std::vector<int> foo(std::vector<int> x) {
return x;
}

and we can even block the move:

std::vector<int> foo() {
std::vector<int> x = {1,2,3,4};
return (std::vector<int> const&)x;
}

or even:

std::vector<int> foo() {
std::vector<int> x = {1,2,3,4};
return 0,x;
}

as the rules for implicit move are intentionally fragile. (0,x is a use of the much maligned , operator).

Now, relying on implicit-move not occurring in cases like this last , based one is not advised: the standard committee has already changed an implicit-copy case to an implicit-move since implicit-move was added to the language because they deemed it harmless (where the function returns a type A with a A(B&&) ctor, and the return statement is return b; where b is of type B; at C++11 release that did a copy, now it does a move.) Further expansion of implicit-move cannot be ruled out: casting explicitly to a const& is probably the most reliable way to prevent it now and in the future.

C++ does compiler automatically use std::move constructor for local variable that is going out of scope?

s_ is a lvalue in the initializer s(s_). So the copy constructor will be used to construct s. There is not automatic move like e.g. in return statements. The compiler is not allowed to elide this constructor call.

Of course a compiler can always optimize a program in whatever way it wants as long as observable behavior is unchanged. Since you cannot observe copies of a string being made if you don't take addresses of the string objects, the compiler is free to optimize the copies away in any case.

If you take a constructor argument to be stored in a member by-value, there is no reason not to use std::move.

Are compilers clever enough to std::move variables going out of scope?

The compiler is not permitted to arbitrarily decide to transform an lvalue name into an rvalue to be moved from. It can only do so where the C++ standard permits it to do so. Such as in a return statement (and only when its return <identifier>;).

*to_be_filled = v; will always perform a copy. Even if it's the last statement that can access v, it is always a copy. Compilers aren't allowed to change that.

My understanding is that return v is O(1), since NRVO will (in effect) make v into an rvalue, which then makes use of std::vector's move-constructor.

That's not how it works. NRVO would eliminate the move/copy entirely. But the ability for return <identifier>; to be an rvalue is not an "optimization". It's actually a requirement that compilers treat them as rvalues.

Compilers have a choice about copy elision. Compilers don't have a choice about what return <identifier>; does. So the above will either not move at all (if NRVO happens) or will move the object.

Is there a subtle reason why not?

One reason this isn't allowed is because the location of a statement should not arbitrarily change what that statement is doing. See, return <identifier>; will always move from the identifier (if it's a local variable). It doesn't matter where it is in the function. By virtue of being a return statement, we know that if the return is executed, nothing after it will be executed.

That's not the case for arbitrary statements. The behavior of the expression *to_be_filled = v; should not change based on where it happens to be in code. You shouldn't be able to turn a move into a copy just because you add another line to the function.

Another reason is that arbitrary statements can get really complicated really quickly. return <identifier>; is very simple; it copies/moves the identifier to the return value and returns.

By contrast, what happens if you have a reference to v, and that gets used by to_be_filled somehow. Sure that can't happen in your case, but what about other, more complex cases? The last expression could conceivably read from a reference to a moved-from object.

It's a lot harder to do that in return <identifier>; cases.

Why is std::forward needed, can't the compiler do the correct thing by default

As a named parameter, param is always an lvalue. That means without std::forward, process(param); will always call the lvalue overload.

On the other hand, you need to tell the compiler when you want to convert it to rvalue explicitly; the compiler can't make the decision for you. e.g.

process(param);                  // you don't want param to be passed as rvalue and thus might be moved here
...
process(std::forward<T>(param)); // it's fine to be moved now

Will compilers apply move semantics automatically in a setter method?

[is] the compiler [...] allowed to automatically use the move constructor

Yes, it would be nice. But this is not only an optimization, this has real impact on the language.

Consider a move-only type like unique_ptr:

std::unique_ptr<int> f()
{
std::unique_ptr<int> up;
return up; // this is ok although unique_ptr is non-copyable.
}

Let's assume your rule would be included into the C++ standard, called the rule of "argument's last occurence".

void SetString(std::unique_ptr<int> data)
{
m_data = data; // this must be ok because this is "argument's last occurence"
}

Checking if an identifier is used in a return is easy. Checking if it is "argument's last occurence" isn't.

void SetString(std::unique_ptr<int> data)
{
if (condition) {
m_data = data; // this is argument's last occurence
} else {
data.foo();
m_data = data; // this is argument's last occurence too
}
// many lines of code without access to data
}

This is valid code too. So each compiler would be required to check for "argument's last occurence", wich isn't an easy thing. To do so, he would have to scan the whole function just to decide if the first line is valid. It is also difficult to reason about as a human if you have to scroll 2 pages down to check this.

No, the compiler isn't allowed to in C++11. And he probably won't be allowed in future standards because this feature is very difficult to implement in compilers in general, and it is just a convenience for the user.

c++11 Return value optimization or move?

Use exclusively the first method:

Foo f()
{
Foo result;
mangle(result);
return result;
}

This will already allow the use of the move constructor, if one is available. In fact, a local variable can bind to an rvalue reference in a return statement precisely when copy elision is allowed.

Your second version actively prohibits copy elision. The first version is universally better.

Why are move semantics necessary to elide temporary copies?

Move functions do not elide temporary copies, exactly.

The same number of temporaries exists, it's just that instead of calling the copy constructor typically, the move constructor is called, which is allowed to cannibalize the original rather than make an independent copy. This may sometimes be vastly more efficient.

The C++ formal object model is not at all modified by move semantics. Objects still have a well-defined lifetime, starting at some particular address, and ending when they are destroyed there. They never "move" during their life time. When they are "moved from", what is really happening is the guts are scooped out of an object that is scheduled to die soon, and placed efficiently in a new object. It may look like they moved, but formally, they didn't really, as that would totally break C++.

Being moved from is not death. Move is required to leave objects in a "valid state" in which they are still alive, and the destructor will always be called later.

Eliding copies is a totally different thing, where in some chain of temporary objects, some of the intermediates are skipped. Compilers are not required to elide copies in C++11 and C++14, they are permitted to do this even when it may violate the "as-if" rule that usually guides optimization. That is even if the copy ctor may have side effects, the compiler at high optimization settings may still skip some of the temporaries.

By contrast, "guaranteed copy ellision" is a new C++17 feature, which means that the standard requires copy ellision to take place in certain cases.

Move semantics and copy ellision give two different approaches to enabling greater efficiency in these "chain of temporaries" scenarios. In move semantics, all the temporaries still exist, but instead of calling the copy constructor, we get to call a (hopefully) less expensive constructor, the move constructor. In copy ellision, we get to skip some of the objects all together.

Basically, why are move semantics considered special and not just a compiler optimization that could have been performed by pre-C++11 compilers?

Move semantics are not a "compiler optimization". They are a new part of the type system. Move semantics happens even when you compile with -O0 on gcc and clang -- it causes different functions to be called, because, the fact that an object is about to die is now "annotated" in the type of reference. It allows "application level optimizations" but this is different from what the optimizer does.

Maybe you can think of it as a safety-net. Sure, in an ideal world the optimizer would always eliminate every unnecessary copy. Sometimes, though, constructing a temporary is complex, involves dynamic allocations, and the compiler doesn't see through it all. In many such cases, you will be saved by move semantics, which might allow you to avoid making a dynamic allocation at all. That in turn may lead to generated code that is then easier for the optimizer to analyze.

The guaranteed copy ellision thing is sort of like, they found a way to formalize some of this "common sense" about temporaries, so that more code not only works the way you expect when it gets optimized, but is required to work the way you expect when it gets compiled, and not call a copy constructor when you think there shouldn't really be a copy. So you can e.g. return non-copyable, non-moveable types by value from a factory function. The compiler figures out that no copy happens much earlier in the process, before it even gets to the optimizer. This is really the next iteration of this series of improvements.



Related Topics



Leave a reply



Submit