为什么这个RAII仅移动类型没有正确模拟`std :: unique_ptr`?

时间:2015-05-26 19:11:55

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

我从this question获取代码并编辑它以通过显式调用其中一个移动构造对象的析构函数来生成段错误:

using namespace std;

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

    Foo(const Foo& f) = delete;

    Foo(Foo&& f) :
      s{f.s}
    {
        cout << "Move ctor called!" << endl;   
        f.s = nullptr;
    }

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

    char* s;
};

void work(Foo&& f2)
{
    cout << "About to create f3..." << endl;
    Foo f3(move(f2));
    // f3.~Foo();
}

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

编译并运行此代码(使用G ++ 4.9)会产生以下输出:

Constructor called!
About to create f3...
Move ctor called!
Destructor called!
s null? 0
Destructor called!
s null? 0
*** glibc detected *** ./a.out: double free or corruption (!prev): 0x0916a060 ***

请注意,当未显式调用析构函数时,不会发生双重自由错误。

现在,当我将s的类型更改为unique_ptr<char[]>并删除delete[] s中的~Foo()f.s = nullptr中的Foo(Foo&&)时(请参阅完整代码如下),我获得双重免费错误:

Constructor called!
About to create f3...
Move ctor called!
Destructor called!
s null? 0
Destructor called!
s null? 1
Destructor called!
s null? 1

这里发生了什么?为什么移动对象在其数据成员为unique_ptr时可以被显式删除,而不是在Foo(Foo&&)中手动处理移动对象的无效时?由于在创建f3时调用了move-constructor (如#34;移动ctor所示!&#34;行),为什么第一个析构函数调用(推测为for f3)状态s null?如果答案只是f3f2由于优化而在某种程度上实际上是同一个对象,那么unique_ptr正在做什么以防止同一问题发生在该实现中?

编辑:根据要求,使用unique_ptr的完整代码:

using namespace std;

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

    Foo(const Foo& f) = delete;

    Foo(Foo&& f) :
      s{move(f.s)}
    {
        cout << "Move ctor called!" << endl;   
    }

    ~Foo() 
    { 
        cout << "Destructor called!" << endl;   
        cout << "s null? " << (s == nullptr) << endl;
    }

    unique_ptr<char[]> s;
};

void work(Foo&& f2)
{
    cout << "About to create f3..." << endl;
    Foo f3(move(f2));
    f3.~Foo();
}

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

我仔细检查过这会产生上面复制的输出。

EDIT2:实际上,使用Coliru(参见下面的T.C。链接),这个确切的代码会产生双重删除错误。

2 个答案:

答案 0 :(得分:5)

对于任何具有非平凡析构函数的类,销毁它两次是核心语言规则的未定义行为:

[basic.life] / P1:

  

类型T的对象的生命周期在以下时间结束:

     
      
  • 如果T是具有非平凡析构函数(12.4)的类类型,则析构函数调用将启动,或者
  •   
  • 对象占用的存储空间被重用或释放。
  •   

[class.dtor] / P15:

  

如果为对象调用析构函数,则行为未定义   其寿命已经结束(3.8)

您的代码两次销毁f3,一次是通过显式析构函数调用,一次是通过离开范围,因此它具有未定义的行为。

libstdc ++和libc ++的unique_ptr析构函数都会分配一个指向存储指针的空指针(libc ++调用reset(); libstdc ++手动完成)。这不是标准所要求的,并且可以说是一个性能错误,它意味着对原始指针的零开销包装。因此,您的代码在-O0中“有效”。

但是,-O2处的g ++能够看到析构函数中的赋值不能被定义良好的程序观察到,因此它会优化分配,导致双重删除。

答案 1 :(得分:3)

如果你明确地调用析构函数,当f3超出范围时,它会被第二次隐式调用。这会创建UB,这就是您的课程崩溃的原因。

您可以在delete中解决崩溃问题,方法是在析构函数中将s重置为nullptr(以便第二次nullptr),但调用中的UB析构函数两次仍然存在。