11

My understanding is that in C++11, when you return a local variable from a function by value, the compiler is allowed to treat that variable as an r-value reference and 'move' it out of the function to return it (if RVO/NRVO doesn't happen instead, of course).

My question is, can't this break existing code?

Consider the following code:

#include <iostream>
#include <string>

struct bar
{
  bar(const std::string& str) : _str(str) {}
  bar(const bar&) = delete;
  bar(bar&& other) : _str(std::move(other._str)) {other._str = "Stolen";}
  void print() {std::cout << _str << std::endl;}

  std::string _str;
};

struct foo
{
  foo(bar& b) : _b(b) {}
  ~foo() {_b.print();}

  bar& _b;
};

bar foobar()
{
  bar b("Hello, World!");
  foo f(b);

  return std::move(b);
}

int main()
{
  foobar();
  return EXIT_SUCCESS;
}

My thoughts were that it would be possible for a destructor of a local object to reference the object that gets implicitly moved, and therefore unexpectedly see an 'empty' object. I tried to test this (see http://ideone.com/ZURoeT ), but I got the 'correct' result without the explicit std::move in foobar(). I'm guessing that was due to NRVO, but I didn't try to rearrange the code to disable that.

Am I correct in that this transformation (causing a move out of the function) happens implicitly and could break existing code?

UPDATE Here is an example which illustrates what I'm talking about. The following two links are for the same code. http://ideone.com/4GFIRu - C++03 http://ideone.com/FcL2Xj - C++11

If you look at the output, it's different.

So, I guess this question now becomes, was this considered when adding implicit move to the standard, and it was decided that it was OK to add this breaking change as this kind of code is rare enough? I also wonder if any compilers will warn in cases like this...

5
  • A move must always leave the object in a destructible state.
    – Zan Lynx
    Oct 7, 2014 at 0:32
  • Yeah, but that's not the question. Pre-c++11 code could assume that the value of a local variable would not change simply due to returning it, so this implicit move could break that assumption.
    – Bwmat
    Oct 7, 2014 at 0:37
  • That's what I tried to elucidate in my example; via destructors you can inspect the state of (a subset of) a function's local variables 'after' the return statement executes, but before the function actually returns.
    – Bwmat
    Oct 7, 2014 at 5:33
  • This is an excellent question with the example you added. I hope this gets more answers from pros who can elucidate this. The only real feedback I can give is: this is why objects shouldn't usually have non-owning views into data. There's actually many ways to write innocent looking code that segfaults when you give objects non-owning views (raw pointers or references). I can elaborate on this in a proper answer if you'd like, but I'm guessing that's not what you want to really hear about. And btw, it's already known that 11 can break existing code, just e.g. by defining new keywords. Oct 7, 2014 at 23:20
  • Yeah, I know that C++11 never claimed to not break any old code, but this is pretty subtle, and would be really easy to miss (no compiler errors, warnings, segfaults)
    – Bwmat
    Oct 7, 2014 at 23:22

1 Answer 1

8

Scott Meyers posted to comp.lang.c++ (August 2010) about a problem where implicit generation of move constructors could break C++03 class invariants:

struct X
{
  // invariant: v.size() == 5
  X() : v(5) {}

  ~X() { std::cout << v[0] << std::endl; }

private:    
  std::vector<int> v;
};

int main()
{
    std::vector<X> y;
    y.push_back(X()); // X() rvalue: copied in C++03, moved in C++0x
}

Here the problem is that in C++03, X had an invariant that its v member always had 5 elements. X::~X() counted on that invariant, but the newly-introduced move constructor moved from v, thereby setting its length to zero.

This is related to your example since the broken invariant is only detected in the X's destructor (as you say it's possible for a destructor of a local object to reference the object that gets implicitly moved, and therefore unexpectedly see an empty object).

C++11 try to achieve a balance between breaking some of existing code and providing useful optimizations based on move constructors.

Committee initially decided that move constructors and move assignment operators should be generated by the compiler when not provided by the user.

Then decided that this was indeed cause for alarm and it restricted the automatic generation of move constructors and move assignment operators in such a way that it is much less likely, though not impossible, for existing code to break (e.g. explicitly defined destructor).

It’s tempting to think that preventing the generation of implicit move constructors when a user-defined destructor is present is enough but it's not true (N3153 - Implicit Move Must Go for further details).

In N3174 - To Move or not to Move Stroupstrup says:

I consider this a language design problem, rather than a simple backwards compatibility problem. It is easy to avoid breaking old code (e.g. just remove move operations from C++0x), but I see making C++0x a better language by making move operations pervasive a major goal for which it may be worth breaking some C++98 code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.