10

我对使用 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,它接受一个 rvalue f2
  3. Foo调用移动构造函数,将所有资源移动f1f2.
  4. Nowf2的析构函数被调用,释放所有资源。
  5. Nowf1的析构函数被调用,它实际上并没有做任何事情,因为所有资源都被转移到了f2. 尽管如此,还是调用了析构函数。

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

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

4

2 回答 2

9

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

原始答案,为后代保存

为了实际执行移动,你必须有类似Foo f3(std::move(f2));inside的东西work。然后你可以调用你的成员函数,f3它是一个新对象,通过从移动创建f

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

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

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

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

于 2010-11-02T21:07:25.150 回答
2

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

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

也许这篇文章会带来更多关于右值引用语义的信息。

于 2010-11-02T21:05:38.443 回答