【问题标题】:Is move constructor called twice in C++?移动构造函数在 C++ 中被调用了两次吗?
【发布时间】:2016-06-02 14:17:44
【问题描述】:

看看这段代码:

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(将 Hello 移入 -> )

[3] DTOR 的 Hello

[4] MOVE CTOR(将 Hello ### 移动到 ->)

[5] DTOR of Hello ###

[6] 现在两个等于:你好### ###

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

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

函数 f() 构造一个临时 [1] 并返回它调用移动构造函数将资源从该临时“移动”到 myObject [2] (另外,它添加了 3 # 个符号)。

最终,临时对象被销毁[3]


我现在希望 myObject 完全构造,并且它的 name 属性是 Hello ###

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

【问题讨论】:

  • 1) return 语句的操作数移入返回值,2) 返回值移入myObject
  • 另一个移动是从f()myObject,因为复制省略被禁用。

标签: c++ c++11 move move-semantics move-constructor


【解决方案1】:

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

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

如果你使用 braced-init-list 来构造返回值,那么只有一个移动构造:

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

【讨论】:

  • 你能告诉我更多关于第 n.1 点的信息吗? 将临时值移入返回值。return Foo("hello")Foo obj("hello")有什么区别return obj
  • @gedamial 第一个会将临时的Foo 移动到返回值中,因为Foo("Hello") 是一个右值,第二个会将obj 复制到返回值中,因为obj 是一个左值。 (当然,只有在禁用复制省略的情况下)
  • 同时return {"Hello"}; 直接将返回值移动到myObject中?所有这些规则都很难记住。有文件吗?
  • @gedamial 那种; return {"Hello"}; 直接初始化返回值,然后移入myObject。您可以查看cppreference documentation 的返回语句。
  • 它是一个左值,但它首先被视为右值,因为它位于复制省略上下文中,即使您禁用了复制省略也是如此。我链接到的文档中有关于此规则的注释。
【解决方案2】:

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

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

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

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

为什么在禁用复制省略的情况下在创建返回值对象时调用移动构造函数?毕竟上面的localObject是一个左值。

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

当满足删除复制/移动操作的条件时,但 不是异常声明,要复制的对象是 由左值指定,或 当表达式在 return 语句中时 是一个(可能用括号括起来的)id 表达式,它用 在正文中声明的自动存储持续时间或 最里面的封闭函数的参数声明子句或 lambda-expression, 重载分辨率以选择构造函数 首先执行复制,就好像对象是由 右值

...

[注意:这个两阶段的重载解析必须执行 无论是否会发生复制省略。它决定了 如果不执行省略,则调用构造函数,并且选择的 即使省略了调用,构造函数也必须是可访问的。 ——尾注 ]

【讨论】:

  • 考虑这个例子:prntscr.com/bbgvzw "then gets moved into the return value placeholder",既然是LValue,为什么是移动而不是复制呢?
  • 它实际上是一个右值。如果您像这样单独创建它:Foo local("hello"); return local; 那么它将是一个左值
  • 如果你看截图,我确实单独创建了它,但它仍然是要调用的移动构造函数
  • 从 f() 到它的返回值占位符。碰巧这在 main 中被忽略了,但编译器不知道这一点。移动的代码是在f()中生成的,可以从其他不忽略返回值的地方调用。
  • 感谢你们,我现在学到了一些奇怪的东西:每个函数都有一个返回占位符,在被发送到函数之外之前要填充。这听起来很奇怪和复杂,有没有关于这个的文档/指南? (en.cppreference.com/w/cpp/language/return除外)
猜你喜欢
  • 2021-08-06
  • 2019-08-02
  • 1970-01-01
  • 2012-06-11
  • 1970-01-01
  • 2019-05-27
  • 1970-01-01
  • 2019-05-09
  • 1970-01-01
相关资源
最近更新 更多