2
struct Test {
    int field = 30;
    Test() { cout << "In ctor" << endl; }
    Test(const Test &other) { field = other.field; cout << "In copy ctor" << endl; }
    Test(Test &&other) { field = other.field; cout << "In move ctor" << endl; }
    Test &operator=(const Test &other) { field = other.field; cout << "In copy assignment" << endl; return *this; }
    Test &operator=(Test &&other) { field = other.field; cout << "In move assignment" << endl; return *this; }
    ~Test() { cout << "In dtor" << endl; }
};

Test get_test() {
    Test t;
    return t;
}

int main() {
    Test t2 = get_test();
}

I think this is the canonical NRVO example. I'm compiling with -fno-elide-constructors and I see that the following are called: ctor, move ctor, dtor, dtor.

So the first ctor call corresponds to the line Test t;, the move ctor is constructing the t2 in main, then the temporary that is returned from get_test is destroyed, then the t2 in main is destroyed.

What I don't understand is: shouldn't there be a copy ctor invocation when returning by value? That is, I thought that get_test should be making a copy of t and then this copy is moved into t2. It seems like t is moved into t2 right away.

bun9
  • 161
  • 8
  • 6
    Copy elision is mandatory beginning with C++17. The compiler is not allowed to call a copy constructor here. The code should compile even if `Test` is not copyable, only movable. – Igor Tandetnik May 16 '22 at 03:27
  • 1
    Oh cool, didn't know that! I compiled with C++14 (just out of curiosity) and now get: ctor, move ctor, dtor, move ctor, dtor, dtor. I'm still not sure I understand that. I know `t` can be moved into the return value since it is going out of scope soon anyway but I thought the whole point of disabling elision is that without elision, returning by value is a copy, not a move. – bun9 May 16 '22 at 03:32
  • 1
    `get_test()` returns a temporary. Initializing from a temporary would call move constructor where available. That's kinda the whole point - it it weren't used for this, what would it be used for? – Igor Tandetnik May 16 '22 at 03:34

1 Answers1

3

C++17

Starting from C++17, there is mandatory copy elison which says:

Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible:

  • In the initialization of an object, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:

(emphasis mine)

This means that t2 is constructed directly from the prvalue that get_test returns. And since a prvalue is used to construct t2, the move constructor is used. Note that in C++17, the flag -fno-elide-constructors have no effect on return value optimization(RVO) and is different from NRVO.


Pre-C++17

But prior to C++17, there was non-mandatory copy elison and since you've provided the -fno-elide-constructors flag, a temporary prvalue is returned by get_test using the move constructor. So you see the first call to the move ctor. Then, that temporary is used to initialize t2 again using the move constructor and hence we get the second call to the move ctor.

Jason
  • 36,170
  • 5
  • 26
  • 60