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

27
看看这段代码:
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(你好) [2] MOVE CTOR(将你好移动到 -> ) [3] DTOR of Hello(你好的析构函数) [4] MOVE CTOR(将你好 ### 移动到 -> ) [5] DTOR of Hello ###(你好 ### 的析构函数) [6] 现在两个相等:Hello ### ### [7] DTOR of Hello ### ###(你好 ### ### 的析构函数)
重要提示:为了测试目的,我已禁用了复制省略优化(使用 -fno-elide-constructors)。
函数 f() 构造了一个临时对象 [1] 并通过调用移动构造函数将资源从该临时对象移动到 myObject [2](此外,它还添加了 3 个 # 符号)。
最后,临时对象被销毁 [3]。
我现在期望myObject已经完全构建,并且它的name属性是"Hello ###"。
然而,移动构造函数再次被调用,所以我得到的是"Hello ### ###"。

6
  1. return语句的操作数被移动到返回值中。
  2. 返回值被移动到myObject中。
- Kerrek SB
еҸҰдёҖдёӘ移еҠЁжҳҜд»Һf()еҲ°myObjectпјҢеӣ дёәеӨҚеҲ¶зңҒз•Ҙиў«зҰҒз”ЁгҖӮ - kennytm
Visual C++在禁用优化的情况下根本不调用移动构造函数。 - undefined
看起来C++17中发生了变化(即,你的C++11标签是正确且重要的)。我发现这篇博客文章非常易懂且有指导性。(在C++17中,你可以Foo(Foo&& ) = delete;移动构造函数,程序仍然可以运行。) - undefined
2个回答

27

两个移动构造函数调用如下:

  1. 将由 Foo("Hello") 创建的临时对象移动到返回值中。
  2. 将由 f() 调用返回的临时对象移动到 myObject 中。

如果您使用了一个 花括号初始化列表 来构造返回值,那么只会有一个移动构造函数调用:

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

这将输出:

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

演示链接


同时,return {"Hello"}; 直接将返回值移动到myObject中?所有这些规则都很难记住。有没有文档可以参考? - gedamial
2
@gedamial 是这样的;return {"Hello"}; 直接初始化了返回值,然后将其移动到 myObject。您可以查看 cppreference 文档 以获取更多关于 return 语句的信息。 - TartanLlama
那么,即使一个返回简单 int myInt{4}; 的函数,也必须将该 lvalue 复制到返回值中,对吗?(当然,如果禁用了复制省略) - gedamial
@gedamial 啊,抱歉,我的错误,在返回语句中,如果符合自C++11以来的复制省略规则,则操作数首先被视为rvalue。(你知道你说过所有这些规则都很难记住吗?是的) - TartanLlama
1
它是一个左值,但因为在复制省略上下文中,即使您禁用了复制省略,它首先被视为右值。我在链接的文档中有关于这个规则的注释。 - TartanLlama
显示剩余5条评论

10
因为您关闭了复制省略,所以您的对象首先在f()中创建,然后被移动到f()的返回值占位符中。此时,f的本地副本被销毁。接下来将返回对象移动到myObject中,并被销毁。最后,myObject被销毁。
如果您没有禁用复制省略,则会看到您期望的顺序。
更新:为了回答下面的评论中的问题,给出了这样一个函数的定义:
Foo f()
{
    Foo localObject("Hello");
    return localObject;
}

禁用了复制省略,为什么移动构造函数会在返回值对象的创建中被调用呢?毕竟,上面的localObject是一个左值。

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

当满足复制/移动操作的省略标准时,但不适用于异常声明,且要复制的对象由左值指定,或者当返回语句中的表达式是一个(可能带括号的)ID表达式, 命名了一个在内层最近的函数或lambda-expression的参数声明子句或 自动存储期间声明的对象,首先执行重载决议以选择复制的构造函数,就好像对象是由一个右值指定一样。

...

[注意:无论是否发生复制省略,都必须执行这两个阶段的重载解析。如果省略不会发生,则确定要调用的构造函数;如果调用被省略,则所选构造函数必须可访问。——注]


考虑这个例子:http://prntscr.com/bbgvzw“然后被移动到返回值占位符中”,既然它是一个LValue,为什么要移动而不是复制? - gedamial
如果您查看屏幕截图,我确实将其单独创建,但仍然会调用Move构造函数。 - gedamial
抱歉,我指的是你问题中的代码。让我有机会查看截图后再回复你。 - Smeeheey
1
从 f() 到其返回值占位符。恰巧在主函数中这被忽略了,但编译器并不知道。移动的代码是在 f() 中生成的,而 f() 可能会被其他不忽略返回值的地方调用。 - Smeeheey
1
感谢你们,我现在学到了一些奇怪的东西:每个函数都有一个返回占位符,在发送到函数外部之前必须填充它。这听起来非常奇怪和复杂,是否有任何关于此的文档/指南?(除了http://en.cppreference.com/w/cpp/language/return) - gedamial
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接