【发布时间】: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<char[]> 并删除~Foo() 中的delete[] s 和Foo(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
这里发生了什么?为什么当它的数据成员是unique_ptr 时可以显式删除被移动对象,而在Foo(Foo&&) 中手动处理被移动对象的无效时却不能?由于移动构造函数 is 在创建 f3 时被调用(如“移动 ctor 调用!”行所示),为什么第一个析构函数调用(可能是 f3)声明 @ 987654337@ 不为空?如果答案很简单,由于优化,f3 和 f2 实际上是同一个对象,那么 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->~T();配对T* t = new(&location) T(args...);.
标签: c++ c++11 move-semantics unique-ptr