26

Look at this code:

class Foo
{
public:

    string name;

    Foo(string n) : name{n}
    {
        cout << "CTOR (" << name << ")" << endl;
    }

    Foo(Foo&& moved)
    {
        cout << "MOVE CTOR (moving " << moved.name << " into -> " << name << ")" << endl;

        name = moved.name + " ###";
    }

    ~Foo()
    {
        cout << "DTOR of " << name << endl;
    }
};

Foo f()
{
    return Foo("Hello");
}

int main()
{
    Foo myObject = f();

    cout << endl << endl;
    cout << "NOW myObject IS EQUAL TO: " << myObject.name;
    cout << endl << endl;

    return 0;
}

The output is:

[1] CTOR (Hello)

[2] MOVE CTOR (moving Hello into -> )

[3] DTOR of Hello

[4] MOVE CTOR (moving Hello ### into -> )

[5] DTOR of Hello ###

[6] NOW two IS EQUAL TO: Hello ### ###

[7] DTOR of Hello ### ###

Important note: I have disabled the copy elision optimization using -fno-elide-constructors for testing purposes.

The function f() constructs a temporary [1] and returns it calling the move constructor to "move" the resources from that temporary to myObject [2] (additionally, it adds 3 # symbols).

Eventually, the temporary is destructed [3].


I now expect myObject to be fully constructed and its name attribute to be Hello ###.

Instead, the move constructor gets called AGAIN, so I'm left with Hello ### ###

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
gedamial
  • 1,498
  • 1
  • 15
  • 30
  • 6
    1) the operand of the `return` statement is moved into the return value, 2) the return value is moved into `myObject`. – Kerrek SB Jun 02 '16 at 14:20
  • The other move is from `f()` into `myObject`, because copy-elision is disabled. – kennytm Jun 02 '16 at 14:22

2 Answers2

27

The two move constructor calls are:

  1. Move the temporary created by Foo("Hello") into the return value.
  2. Move the temporary returned by the f() call into myObject.

If you used a braced-init-list to construct the return value, there would only be a single move construction:

Foo f()
{
    return {"Hello"};
}

This outputs:

CTOR (Hello)
MOVE CTOR (moving Hello into -> )
DTOR of Hello    
NOW myObject IS EQUAL TO: Hello ###    
DTOR of Hello ###

Live Demo

Sumurai8
  • 20,333
  • 11
  • 66
  • 100
TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • Will you tell me more about the point n.1? **Move the temporary into the return value.** What's the difference between `return Foo("hello")` and `Foo obj("hello")` `return obj` – gedamial Jun 02 '16 at 14:41
  • @gedamial The first one will move the temporary `Foo` into the return value because `Foo("Hello")` is an rvalue, the second one will copy `obj` into the return value because `obj` is an lvalue. (Only if copy elision is disabled, of course) – TartanLlama Jun 02 '16 at 14:42
  • Meanwhile `return {"Hello"};` **directly** moves the return value into myObject? All these rules are tough to remember. Is there any documentation? – gedamial Jun 02 '16 at 14:44
  • 2
    @gedamial Kind of; `return {"Hello"};` directly initializes the return value, which is then moved into `myObject`. You can have a look at the [cppreference documentation](http://en.cppreference.com/w/cpp/language/return) for return statements. – TartanLlama Jun 02 '16 at 14:47
  • So even a function returning a simple `int myInt{4};` has to copy that lvalue into the return value, right? (if copy-elision is disabled, of course) – gedamial Jun 02 '16 at 14:50
  • @gedamial Theoretically, yes. In practice, the constant can just be propagated if it's known at compile time. – TartanLlama Jun 02 '16 at 14:53
  • A function that creates a Foo object **moves** that object into the return value. I wonder: why? The object is an lvalue because gets constructed **first** and **then** returned. Shouldn't it be copied? http://prntscr.com/bbgvzw – gedamial Jun 02 '16 at 14:58
  • @gedamial Ah, sorry, my mistake, in a return statement, the operand is first considered as an rvalue if copy-elision rules are met since C++11. (You know how you said all these rules are tough to remember? Yeah) – TartanLlama Jun 02 '16 at 15:01
  • In my case, if you see the screenshot, I'm creating a local object (lvalue) and I pass it into the return statement. Here, I disabled copy elision, so it should be considered an Lvalue. It seems to be not, since the Move Constructor is called! – gedamial Jun 02 '16 at 15:11
  • 1
    It is an lvalue, but it's first considered as an rvalue because it's in a copy elision context, even though you disabled copy elision. There's a note about this rule in the documentation I linked to. – TartanLlama Jun 02 '16 at 17:16
10

Because you turned off copy elision, your object first gets created in f(), then gets moved into the return value placeholder for f(). At this point f's local copy is destroyed. Next the return object is moved into myObject, and also destroyed. Finally myObject is destroyed.

If you didn't disable copy elision, you would have seen the sequence you expected.

UPDATE: to address question in comment below, which is - given the definition of a function like this:

Foo f()
{
    Foo localObject("Hello");
    return localObject;
}

Why is the move constructor invoked in the creation of the return-value object with copy elision disabled? After all, localObject above is an lvalue.

The answer is that the compiler is obliged in these circumstances to treat the local object as an rvalue, so effectively it is implicitly generating the code return std::move(localObject). The rule that requires it to do so is in the standard [class.copy/32] (relevant parts highlighted):

When the criteria for elision of a copy/move operation are met, but not for an exception-declaration, and the object to be copied is designated by an lvalue, or when the expression in a return statement 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.

...

[ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. — end note ]

Community
  • 1
  • 1
Smeeheey
  • 9,906
  • 23
  • 39
  • Consider this example: http://prntscr.com/bbgvzw "then gets **moved** into the return value placeholder", since it's a LValue, why is it moved and not copied? – gedamial Jun 02 '16 at 15:25
  • It is in fact an rvalue. If you had created it separately like this: `Foo local("hello"); return local;` *then* it would be an lvalue – Smeeheey Jun 02 '16 at 15:42
  • If you look at the screenshot, I **do create it separately**, but it's still the Move Constructor the one to be invoked – gedamial Jun 02 '16 at 15:45
  • Sorry I was referring to the code in your question. Let me get back to you when I have a chance to look at the screenshot – Smeeheey Jun 02 '16 at 15:48
  • Thanks! Last thing: consider my function f(). If I call that function **alone** (without using it to construct any other object, just alone, in the main), the Move Constructor is called once. Why? I mean, where is the Move happening? From f() to what? – gedamial Jun 02 '16 at 19:30
  • 1
    From f() to its return value placeholder. As it happens this is ignored in main but the compiler can't know that. The code to move is generated in f(), which may be called from other places that don't ignore the returned value. – Smeeheey Jun 02 '16 at 19:39
  • 1
    thanks to you guys, I've now learnt something strange: **every function has a return placeholder to be filled before being sent outside the function**. This sounds very strange and complex, is there any documentation/guideline about this? (except http://en.cppreference.com/w/cpp/language/return) – gedamial Jun 02 '16 at 19:42