【问题标题】:Zombie objects after std::movestd::move 之后的僵尸对象
【发布时间】:2011-05-04 04:22:19
【问题描述】:

我对使用 C++0x 移动语义移动对象之后的状态感到困惑。我的理解是,一旦一个对象被移动了,它仍然是一个有效的对象,但是它的内部状态已经改变了,所以当它的析构函数被调用时,没有资源被释放。

但如果我的理解是正确的,移动对象的析构函数应该仍然被调用。

但是,当我执行一个简单的测试时,这不会发生:

struct Foo
{
    Foo()  
    {
        s = new char[100]; 
        cout << "Constructor called!" << endl;  
    }

    Foo(Foo&& f) 
    {
        s = f.s;
        f.s = 0;
    }

    ~Foo() 
    { 
        cout << "Destructor called!" << endl;   
        delete[] s; // okay if s is NULL
    }

    void dosomething() { cout << "Doing something..." << endl; }

    char* s;
};

void work(Foo&& f2)
{
    f2.dosomething();
}

int main()
{
    Foo f1;
    work(std::move(f1));
}

这个输出:

Constructor called!
Doing something...
Destructor called!

注意析构函数只被调用一次。这表明我在这里的理解是错误的。为什么没有调用两次析构函数?这是我对应该发生的事情的解释:

  1. Foo f1 已构造。
  2. Foo f1 被传递给 work,它 采用右值 f2
  3. Foo 的移动构造函数是 调用,移动f1中的所有资源 到f2
  4. 现在f2的析构函数被调用, 释放所有资源。
  5. 现在f1的析构函数被调用, 这实际上并没有做任何事情 因为所有资源都被转移了 到f2。不过,析构函数是 尽管如此。

但由于只调用了一个析构函数,因此第 4 步或第 5 步都不会发生。我从析构函数进行了回溯,以查看它是从哪里调用的,它是从第 5 步调用的。那么为什么不调用f2 的析构函数呢?

编辑:好的,我修改了这个,所以它实际上是在管理一个资源。 (一个内部内存缓冲区。)不过,我得到了与只调用一次析构函数相同的行为。

【问题讨论】:

  • 你在哪个编译器上测试?
  • gcc 4.3 ....................
  • 在这种情况下,值得注意的是它是针对更旧版本的 C++0x 草案编写的。从那时起,移动语义已经发生了很大变化。例如,我相信在您添加 std::move 之前,较新的编译器会完全拒绝您的代码。它无法调用work,因为该参数实际上是一个左值引用,因为它被命名了。
  • @jalf,好的。但是你同意我的观点吗,在当前的 C++0x 草案中,析构函数应该在这里调用两次?
  • @Channel72:假设移动没有被优化掉,是的。

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


【解决方案1】:

请注意,编译器可能会优化掉不需要的构造/破坏,从而有效地使只存在一个对象。对于右值引用(正是为此目的而发明的)尤其如此。

我认为您对所发生事情的解释是错误的。不调用移动构造函数:如果值不是临时值,则右值引用的行为与普通引用一样。

也许this article 会带来更多关于右值引用语义的信息。

【讨论】:

  • 即使我不使用任何优化标志进行编译?
  • @Channel72:C++ 标准不关心优化标志。优化要么是合法的(它尊重 C++ 的语义),要么不是。只要它是合法的,编译器就可以在任何时候应用它,就 C++ 标准而言。当然,如果它不合法,则应该永远应用它。即使禁用了优化,编译器通常也会执行一些小的优化(例如,RVO,在某些编译器上,还有 NRVO)。它可能还会执行复制省略,这可以解释您的结果
  • 我认为根本没有复制,因为您的代码中实际上没有任何临时对象。
  • 是的...我想知道为什么我直到现在才看到。你从不真正要求搬家,你只是传递参考。它们是右值引用而不是左值引用这一事实并没有改变它们仍然是引用,而不是新对象。
【解决方案2】:

编辑 (新的正确答案)
抱歉,仔细查看代码,似乎答案要简单得多: 你永远不会调用移动构造函数。您永远不会真正移动对象。您只需将一个右值引用传递给work 函数,该函数在该引用上调用一个成员函数,它仍然指向原始对象。

原始答案,留作后人

为了实际执行移动,您必须在 work 中包含类似 Foo f3(std::move(f2)); 的内容。然后您可以在f3 上调用您的成员函数,这是一个新对象,通过从f 移动而创建

据我所知,您根本没有获得移动语义。您只是看到了普通的旧副本省略。

要进行移动,您必须使用std::move(或者具体来说,传递给构造函数的参数必须是未命名/临时的)右值引用,例如从std::move 返回的右值引用) .否则它会被视为一个普通的老式左值引用,然后复制应该,但像往常一样,编译器可以优化它,只剩下一个正在构造的对象,一个正在销毁的对象。

无论如何,即使使用移动语义,编译器也没有理由不做同样的事情:只需优化移动,就像它优化了副本一样。移动很便宜,但在需要它的地方构造对象仍然更便宜,而不是构造一个,然后将其移动到另一个位置并在第一个位置调用析构函数。

还值得注意的是,您使用的是相对较旧的编译器,早期版本的规范非常不清楚这些“僵尸对象”应该如何处理。因此,GCC 4.3 可能只是不调用析构函数。我相信只有最后一个版本,或者可能是之前的版本,明确要求调用析构函数

【讨论】:

  • 好的,我添加了对std::move 的显式调用,只是为了确定我确实传递了一个右值。但它不会改变行为。
  • 我没想到会这样。据我所知,编译器也被允许在右值引用上执行复制省略(或移动省略,我猜):在你的简单测试用例中,它可以在适当的位置构造对象,而不是构造一个然后移动它。
  • 嗯...好吧,我编辑了代码,以便 Foo 现在实际管理资源。它分配一个缓冲区并在移动构造函数中转移缓冲区的所有权。但是,析构函数仍然只被调用一次。我在这里做错了吗?
  • @Chanel72:据我所见,仍然没有什么可以阻止编译器优化移动。为什么它根本需要原始对象?您只是立即远离它。
  • 如果我是你,我会定义每种类型的构造函数(包括 atm 编译器生成的复制 ctor)来编写一些输出,这样你就可以准确地看到每个构造函数被调用了多少次.
猜你喜欢
  • 1970-01-01
  • 2011-03-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多