问题
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.
回答1:
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.
回答2:
If you call destructor explicitely, it will be called second time implicitely when f3
goes out of scope. This creates UB and that is why your class crashes.
You can work-around the crash in delete
by resetting s
to nullptr
in the destructor (so that second time it is nullptr
) but the UB in calling destructor twice will be still there.
来源:https://stackoverflow.com/questions/30466949/why-doesnt-this-raii-move-only-type-properly-emulate-stdunique-ptr