为什么这个RAII移动类型不正确模拟`std :: unique_ptr`? [英] Why doesn't this RAII move-only type properly emulate `std::unique_ptr`?

查看:246
本文介绍了为什么这个RAII移动类型不正确模拟`std :: unique_ptr`?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我从此问题中取得代码,并编辑它以产生segfault,通过显式调用其中一个的析构函数移动构造的对象:

  using namespace std; 

struct Foo
{
Foo()
{
s = new char [100];
cout<< 构造函数叫! << endl;
}

Foo(const Foo& f)= delete;

Foo(Foo& f):
s {f.s}
{
cout< 移动ctor叫! << endl;
f.s = nullptr;
}

〜Foo()
{
cout< 析构函数叫! << endl;
cout<< null?< (s == nullptr)<< endl;
delete [] s; // ok如果s是NULL
}

char * s;
};

void work(Foo&& f2)
{
cout< 关于创建f3 ...< endl;
Foo f3(move(f2));
// f3。〜Foo();
}

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

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

 构造函数调用! 
关于创建f3 ...
移动ctor调用!
析构函数调用!
s null? 0
析构函数调用!
s null? 0
*** glibc detected *** ./a.out:double free or corruption(!prev):0x0916a060 ***

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



现在, unique_ptr< char []> 并删除 delete [] s in 〜Foo() fs = nullptr Foo&&&&&&&&&&&&&&&&&&&)(请参阅下面的完整代码),我不会得到双重错误:

 构造函数调用! 
关于创建f3 ...
移动ctor调用!
析构函数调用!
s null? 0
析构函数调用!
s null? 1
析构函数调用!
s null? 1

这是怎么回事?为什么被移动的对象在其数据成员是 unique_ptr 时被显式删除,而在被移动对象的失效在 Foo(Foo&&)?由于当创建 f3 (如Move ctor called!行所示)时,调用移动构造函数,为什么第一个析构函数调用(假设 f3 )说明 s 不是 null?如果答案是简单的 f3 f2 实际上是相同的对象由于优化,什么是 unique_ptr 这样做是为了防止同样的问题发生在该实现中?






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

 code> using namespace std; 

struct Foo
{
Foo():
s {new char [100]}
{
cout< 构造函数叫! << endl;
}

Foo(const Foo& f)= delete;

Foo(Foo& f):
s {move(f.s)}
{
cout< 移动ctor叫! << endl;
}

〜Foo()
{
cout< 析构函数叫! << endl;
cout<< null?< (s == nullptr)<< endl;
}

unique_ptr< char []> s;
};

void work(Foo&& f2)
{
cout< 关于创建f3 ...< endl;
Foo F3(move(f2));
f3。〜Foo();
}

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

我已经仔细检查过了, >

EDIT2:实际上,使用Coliru(请参阅下文的TC链接),此确切代码会产生双重删除错误。

解决方案

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

[basic.life] / p1:


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




  • if T 是一个具有非平凡析构函数(12.4)的类类型,析构函数调用开始,或

  • 对象占用的存储被重用或释放。


[class.dtor] / p15:


如果为对象
调用析构函数的行为未定义


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



发生libstdc ++和libc ++的 unique_ptr 析构函数将为存储的指针分配一个空指针(libc ++调用 reset() code>; libstdc ++手动)。这不是标准要求的,可以说是一个性能错误,这意味着一个零开销的包装器超过裸指针。因此,您的代码在 -O0 中工作。



g ++在 -O2 ,但是,能够看到析构函数中的赋值不可能一个定义良好的程序,所以它优化了分配,导致双删除。


I took the code from this question and edited it to produce a segfault by explicitly calling the destructor of one of the move-constructed objects:

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));
}

Compiling and running this code (with G++ 4.9) produces the following output:

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 ***

Note that when the destructor is not explicitly called, no double-free error occurs.

Now, when I change the type of s to unique_ptr<char[]> and remove the delete[] s in ~Foo() and f.s = nullptr in Foo(Foo&&) (see full code below), I do not get a double-free error:

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

What is going on here? Why can the moved-to object be explicitly deleted when its data member is a unique_ptr, but not when the invalidation of the moved-from object is handled manually in Foo(Foo&&)? Since the move-constructor is called when f3 is created (as shown by the "Move ctor called!" line), why does the first destructor call (presumably for f3) state that s is not null? If the answer is simply that f3 and f2 are somehow actually the same object due to an optimization, what is unique_ptr doing that's preventing the same problem from happening with that implementation?


EDIT: As requested, the full code using 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));
}

I have double-checked that this produces the output copied above.

EDIT2: Actually, using Coliru (see T.C.'s link below), this exact code does produce a double-deletion error.

解决方案

For any class with a non-trivial destructor, destroying it twice is undefined behavior by core language rule:

[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)

Your code destroys f3 twice, once by explicit destructor call and once by leaving the scope, so it has undefined behavior.

It happens that both libstdc++ and libc++'s unique_ptr destructor will assign a null pointer to the stored pointer (libc++ calls reset(); libstdc++ does it manually). This is not required by the standard, and is arguably a performance bug in something that's meant to be an zero-overhead wrapper over raw pointers. As a result, your code "works" in -O0.

g++ at -O2, however, is able to see that the assignment in the destructor cannot possibly be observed by a well-defined program, so it optimizes away the assignment, causing a double delete.

这篇关于为什么这个RAII移动类型不正确模拟`std :: unique_ptr`?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆