为什么这个 RAII 只移动类型不能正确地模拟 std::unique_ptr ?

Why doesn't this RAII move-only type properly emulate `std::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 ***

注意,当析构函数没有显式调用时,不会出现double-free错误。

现在,当我将 s 的类型更改为 unique_ptr<char[]> 并删除 ~Foo() 中的 delete[] sFoo(Foo&&) 中的 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

这是怎么回事?为什么moved-to对象的数据成员是unique_ptr时可以显式删除,而在Foo(Foo&&)中手动处理moved-from对象的失效却不能?由于移动构造函数 在创建 f3 时调用的(如 "Move ctor called!" 行所示),为什么第一个析构函数调用(大概是 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.'s link),这个确切的代码 确实 产生双重删除错误。

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

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

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

[basic.life]/p1:

The lifetime of an object of type T ends when:

  • if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
  • the storage which the object occupies is reused or released.

[class.dtor]/p15:

the behavior is undefined if the destructor is invoked for an object whose lifetime has ended (3.8)

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

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

然而,

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