c++ - 为什么这个 RAII 只 move 类型不能正确模拟 `std::unique_ptr` ?

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

我从 this question 中获取了代码并通过显式调用 move 构造对象之一的析构函数来编辑它以产生段错误:

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[]>并删除 delete[] s~Foo()f.s = nullptrFoo(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

这是怎么回事?为什么 move 对象的数据成员是unique_ptr时可以显式删除? ,但在 Foo(Foo&&) 中手动处理移出对象的失效时则不然?由于 move 构造函数调用时 f3已创建(如“Move ctor called!”行所示),为什么第一个析构函数调用(可能是 f3 )声明 s 是空的吗?如果答案只是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. 的链接),这段代码确实产生了双重删除错误。

最佳答案

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

[基本生活]/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.

[类.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++ 手动执行)。这不是标准所要求的,并且可以说是一个性能错误,它意味着对原始指针的零开销包装。因此,您的代码在 -O0 中“有效”。

但是,

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

关于c++ - 为什么这个 RAII 只 move 类型不能正确模拟 `std::unique_ptr` ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30466949/

相关文章:

c++ - ceil 函数在 C++ 中如何工作?

c++ - 在 move 构造函数之前调用析构函数?

c++ - 返回 std::tuple 并 move 语义/复制省略

c++ - boost::filesystem::path::lexically_normal: 这是不正确的行为吗?

c++ - c++ NULL assignment 赋值运算符

c++ - 如何将文件中的团队信息解析并存储到类中?

c++ - 通过单个函数返回另一个函数的多个参数

带有模板基类的 C++ std::shared_pointer

c++ - 使用 move 语义在构造函数中初始化类成员

C++字符串转二进制代码/二进制代码转字符串