20

A code like this is often seen in r-value references articles:

Dave Abrams: Move It With Rvalue References

void g(X);

void f()
{
    X b;
    g(b);              // still need the value of b
    …
    g( std::move(b) ); // all done with b now; grant permission to move
}

Could a compiler generate this optimization automatically, i.e. to detect a l-value is going to be destructed anyway and could be moved from, or would this be a violation of the standard, assuming a generic case the compiler does not know anything about how is move, copy or destruct implemented for the X class?

If such optimization is allowed, is it performed by some compiler in practice?

Suma
  • 33,181
  • 16
  • 123
  • 191
  • 3
    Yes. No. Maybe. A compiler could do it as long as it would not be a violation of the standard (the so-called "as-if rule"). – R. Martinho Fernandes Mar 13 '13 at 13:54
  • 1
    What the robot said, although the answer will generally be "no". There is a guarantee to get an automatic move if you return a local variable, though (modulo some conditions, like the return type being the same type as the local variable). – Xeo Mar 13 '13 at 13:54
  • @R.MartinhoFernandes Well, the question *specifically* asks whether this would violate the standard, so I'm not quite sure what your comment adds to it. (unless it was edited after you commented?) – us2012 Mar 13 '13 at 13:55
  • 2
    That could change program's observable behaviour by using a different overload of `g()`. – juanchopanza Mar 13 '13 at 13:56
  • @us2012 It would violate the standard if it would be a violation the standard (IOW, the example leaves too much undefined for an answer that isn't "it depends") – R. Martinho Fernandes Mar 13 '13 at 13:56
  • @juanchopanza: If the compiler can prove that the behaviour won't change, as should be with the example here, it can insert a move all-right (you can't rely on side-effects inside copy/move ctors, as those can be elided). – Xeo Mar 13 '13 at 13:57

3 Answers3

15

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.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • Nice counterexample, which shows it is hard for the compiler to prove some particular use of a l-value is really the last before its destruction, because there may still be a pointer or reference to it hanging around. – Suma Mar 13 '13 at 14:13
  • @Suma it's not just access to lvalues; `b` could be a mutex lock guard. See my new example above. – ecatmur Mar 13 '13 at 14:20
  • I think the escape analysis, similar to one used in GC runtimes to prove that memory can be allocated from stack can be employed to prove that any no valid reference to `b` memory is reachable after the call to `g`. Object with custom constructors and may be automagically refused, but for simple things likelike `struct { int x, int y, int z }` escale analysis can be reliable ... i think. – Zbigniew Zagórski Aug 02 '17 at 10:24
  • @ZbigniewZagórski sure, but if the types are simple enough for escape analysis to work then the compiler can optimize out the copy/move anyway. RVO-style rules are needed precisely where the types are complex enough that the side effects on their operations are difficult to track. – ecatmur Aug 02 '17 at 10:33
  • @ecatmur in most of the real world examples I've seen, including OP's example, the final g(X) call is the last instruction of the function. Neither of your counterexamples address that scenario, but AFAIK it would always be safe. Anyway, it is good to know that the compiler isn't going to do this for me (yet). – Patrick Parker Sep 03 '19 at 15:48
  • Nice example, but this addresses **will always**, while the question is **can ever**. What about in simple cases where no references - (or even *usages*) - of/into `b` are made? – c z Jun 08 '23 at 16:08
5

Could a compiler do it? Only as an explicit language extension, because the standard doesn't allow them to make such an optimization without that.

Should they do it? No. The meaning of g(b) should be based on the definition of g and b. g should be some callable type, which has an overload that takes something that b can be implicitly converted into. Given access to the definition of all available gs, and the definition of b, you should be able to determine exactly what function will be called.

To allow this "optimization" now means that this is impossible. g(b) might perform a move, and it might not, depending on exactly where g(b) happens to be in a function. That's not a good thing.

return is allowed to get away with it, but only because it still has the same meaning. return b will always attempt to move from b if b is a value-type who's lifetime is restricted to the scope of the function.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
4

(...) assuming a generic case the compiler does not know anything about how is move, copy or destruct implemented for the X class?

No, compilers are not allowed to do optimisations based on faith.

For clarity, this question is unrelated to copy elision: compilers may be allowed to elide a copy, but they cannot change copies to moves willy-nilly.

R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
  • Well, sometimes they are, e.g. in the case of copy-elision (e.g. Return Value Optimization) - in that case the are allowed to have a faith a programmer has implemented the copy constructor in a reasonable way. I guess in this particular case there is nothing in the standard to allow the compiler to have this "faith", which makes the optimization impossible. – Suma Mar 13 '13 at 14:02
  • 4
    That optimization is not based on faith. It is based on the fact that the Standard explicitly permits them to ignore the consequences. – Puppy Mar 13 '13 at 14:05