【问题标题】:Unexpected destructor call occurs when paired with move constuctor与移动构造函数配对时发生意外的析构函数调用
【发布时间】:2012-09-26 05:10:15
【问题描述】:

作为学习练习,以下代码在 Visual Studio 2012 Express for Windows Desktop 中编译和运行。

#include <cstdio>

class X
{
public:
    X()  { printf("default constructed\n"); }
    ~X() { printf("destructed\n");}
    X(const X&) { printf("copy constructed\n"); }
    X(X&&) { printf("move constructed\n"); }
    X & operator= (const X &) { printf("copy assignment operator\n"); }
};

X A() {
    X x;
    return x;
}

int main() {
    {
        A();
    }
    std::getchar();
}

在禁用编译器优化 (/Od) 的情况下编译时,结果输出表明析构函数被调用了两次。这是一个问题,因为只构造了一个对象。 为什么要调用两次析构函数?如果该类管理自己的资源,这不是问题吗?

default constructed
move constructed
destructed
destructed   <<< Unexpected call 

我尝试了几个实验来尝试解释输出,但最终这些并没有导致任何有用的解释。

实验 1:在启用优化(/O1 或 /O2)的情况下编译相同的代码时,结果输出为:

default constructed
destructed

这表明命名返回值优化已经省略了对移动构造函数的调用,并掩盖了潜在的问题。

实验 2:禁用优化并注释掉移动构造函数。生成的输出符合我的预期。

default constructed
copy constructed
destructed
destructed

【问题讨论】:

  • 你的构造函数和析构函数的数量一样,所以没有问题。它被移动形式的事实是不相关的。对于管理资源的类来说,这不是问题,他们都知道事情就是这样工作的。

标签: c++ visual-c++ c++11 visual-studio-2012 move-semantics


【解决方案1】:

请记住,当一个对象是移动操作的源时,它仍然会被销毁。所以移动的源需要将自己置于一个状态,使得被破坏不会释放它不再拥有的资源(因为它们被移动到另一个对象)。例如,源对象中的任何原始指针(现在将由移动构造的对象拥有)应设置为 NULL。

【讨论】:

    【解决方案2】:

    A 中的 X 在超出范围时被销毁。

    A 返回一个临时对象(由移动构造函数从 X 构造),它是一个单独的实例。这在调用者的范围内被销毁。这将导致再次调用析构函数(临时)。

    选择移动构造函数是因为编译器检测到 X 将在之后立即被销毁。要使用这种方法,移动构造函数应该使原始对象中的任何数据无效或重置,以便析构函数不会使移动目标所接管的任何数据无效。

    当您按值传递右值,或从函数按值返回任何内容时,编译器首先获得删除副本的选项。如果副本没有被省略,但相关类型具有移动构造函数,则编译器需要使用移动构造函数。

    http://cpp-next.com/archive/2009/09/move-it-with-rvalue-references/

    当您退出创建临时对象的范围时,它会被销毁。如果引用绑定到临时对象,则当引用超出范围时临时对象将被销毁,除非它在控制流中断之前被销毁。

    http://publib.boulder.ibm.com/infocenter/comphelp/v8v101/index.jsp?topic=%2Fcom.ibm.xlcpp8a.doc%2Flanguage%2Fref%2Fcplr382.htm

    RVO 可以产生与非优化版本不同的行为:

    返回值优化,或简称为 RVO,是一种编译器优化技术,涉及消除为保存函数返回值而创建的临时对象。[1]在 C++ 中,特别值得注意的是允许更改结果程序的可观察行为。[2]

    http://en.wikipedia.org/wiki/Return_value_optimization

    【讨论】:

    • 澄清一下,使用哪个构造函数来创建临时对象?
    • 你是对的,但为什么没有为临时对象调用构造函数?
    • 移动构造函数初始化临时对象,因此在这种情况下不会调用默认ctor。
    【解决方案3】:

    虽然 Michael 和 jspcal 的答案是准确的,但他们并没有回答我的问题的核心,这就是为什么要调用两个析构函数。我只期待一个。

    答案是函数 A() 返回一个临时对象。总是。这就是函数返回值的工作方式,而移动语义与这一事实无关。我猜迈克尔和 jspcal 认为我没有错过这样一个基本事实。我将“移动”一词等同于“交换”的概念。交换时,不会构造和销毁对象。因此,我只期待一个析构函数调用。

    由于必须构造和析构返回的对象,因此进行了第二次析构函数调用(以及第二次构造函数调用)。

    现在,选择执行的实际构造函数取决于类定义中提供的内容。如果移动构造函数可用,则会调用它。否则将调用复制构造函数。

    【讨论】:

      猜你喜欢
      • 2015-02-21
      • 2017-11-03
      • 2011-05-22
      • 1970-01-01
      • 2017-04-17
      • 2011-04-16
      • 1970-01-01
      • 2015-06-30
      • 2022-01-12
      相关资源
      最近更新 更多