移动构造函数在 C++ 中被调用两次吗?

Is move constructor called twice in C++?

看这段代码:

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;
}

输出为:

[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 ### ###

重要说明:出于测试目的,我已使用 -fno-elide-constructors 禁用复制省略优化。

函数 f() 构造一个临时 [1] 并且 returns 它调用移动构造函数 "move" 将资源从该临时到 myObject [2] (另外,它增加了3个#符号)。

最终,临时文件被销毁[3]


我现在希望 myObject 完全构建,它的 name 属性是 Hello ###.

相反,移动构造函数被再次调用,所以我只剩下 Hello ### ###

因为您关闭了复制省略,您的对象首先在 f() 中创建,然后被移动到 f() 的 return 值占位符中。此时f的本地副本被销毁。接下来 return 对象被移入 myObject,并且也被销毁。最后myObject被销毁。

如果您没有禁用复制省略,您会看到预期的序列。

UPDATE:解决下面评论中的问题,即 - 给定这样的函数定义:

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

为什么在禁用复制省略的 return-value 对象的创建中调用移动构造函数?毕竟上面的localObject是一个左值

答案是编译器在这些情况下有义务将本地对象视为右值,因此它实际上是隐式生成代码 return std::move(localObject)。要求它这样做的规则在标准 [class.copy/32] 中(相关部分突出显示):

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 ]

两个移动构造函数调用是:

  1. Foo("Hello") 创建的临时文件移动到 return 值中。
  2. 将由 f() 调用编辑的临时 return 移动到 myObject

如果您使用 braced-init-list 来构造 return 值,则只有一个移动构造:

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

这输出:

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

Live Demo