【问题标题】:Why doesn't this RAII move-only type properly emulate `std::unique_ptr`?为什么这个 RAII 只移动类型不能正确模拟 `std::unique_ptr`?
【发布时间】:2015-08-08 14:57:28
【问题描述】:

我从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&lt;char[]&gt; 并删除~Foo() 中的delete[] sFoo(Foo&amp;&amp;) 中的f.s = nullptr(请参阅下面的完整代码)时,我会不 得到一个双释放错误:

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&amp;&amp;) 中手动处理被移动对象的无效时却不能?由于移动构造函数 is 在创建 f3 时被调用(如“移动 ctor 调用!”行所示),为什么第一个析构函数调用(可能是 f3)声明 @ 987654337@ 为空?如果答案很简单,由于优化,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. 的链接),这个确切的代码确实会产生双重删除错误。

【问题讨论】:

  • 为什么要显式调用析构函数?这没有道理。当然,如果你这样做,事情会被双重删除。
  • 请发布完整的unique_ptr 版本,因为I can't reproduce this。另外,如果unique_ptr 的析构函数被编写为容忍双重破坏,我实际上会认为这是一个性能错误。
  • 两次销毁unique_ptr 是调用未定义的行为。我不确定我们能否根据结果了解更多关于正确性的知识。
  • 看起来像this depends on the optimizer settings。经典 UB。
  • “如您所见,一个设计良好的只移动类型对于这样的使用是健壮的。”额外删除一个已移动的类型是永远不会安全的。显式调用析构函数应与在对象位置上显式调用placement new 配对(之前或之后 - 和之后总是值得怀疑的!)就像t = new T(args...)delete t; 配对,t-&gt;~T(); 配对T* t = new(&amp;location) T(args...);.

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


【解决方案1】:

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

[basic.life]/p1:

T 类型对象的生命周期结束于:

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

[class.dtor]/p15:

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

您的代码破坏了f3 两次,一次通过显式析构函数调用,一次通过离开作用域,因此它具有未定义的行为。

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

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-16
      • 2015-08-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多