The code above works if compiled with Visual Studio. If the destructor throws an exception the operator delete
is not called. If the destructor doesn’t throw an exception the operator delete
is called. But the same code causes Segmentation fault if compiled with gcc on Linux. It doesn’t work even if there is no std::string
member in the class. It terminates with "free(): double free detected in tcache 2". This means that the operator delete
is called even if the destructor throws an exception. Is the behavior undefined according to the C++ standard? If so, does it mean that delete
cannot be called again if the first call threw an exception?
#include <string>
#include <iostream>
class MyClass
{
public:
void *operator new(size_t size)
{
void *p = std::malloc(size);
if (!p) throw std::bad_alloc();
return p;
}
void operator delete(void *p)
{
std::free(p);
}
~MyClass() noexcept (false)
{
if (s == "123") throw int{};
}
std::string s{"123"};
};
int main()
{
MyClass* myobject = new MyClass{};
try
{
delete myobject;
}
catch (...) {}
std::cout << "<" << myobject->s << ">";
myobject->s = "abc";
delete myobject;
return 0;
}
I compiled and ran the code on both Windows using Visual Studio and Linux using GCC. I expected it to work on both platforms, but it didn’t work on Linux with GCC
2
Answers
GCC is correct and MSVC is wrong (or would be wrong if the code worked on it, it doesn’t for me locally).
In
[expr.delete]
there’s this:Notes are not normative, but you can read the normative wording right above if you want.
So the solution (other than not throwing from destructors in the first place) is to not use
delete
, but rather manually call the destructor usingmyobject->~MyClass()
, catch any exceptions, thenfree()
the memory. Then I would also replacenew
with malloc + placement new (for symmetry, and because the wording suggestsnew
/delete
allocations can be elided).delete
anddelete[]
call the deallocation function even if any destructor call exits with an exception.If a destructor exits by throwing an exception, then the lifetime of the object has still ended. You can’t try to destruct it again. The only sensible thing one can do in this scenario is to release the memory of the object anyway. The object isn’t usable any more at all. This is also why having destructors exit by exceptions is generally a bad idea.
Because you try to call
delete
again on the same object, which involves attempting to call the destructor again and the deallocation function again, your program has undefined behavior.It is possible that a compiler elided both the
malloc
andfree
call(s) after inlining, either because it noticed that the memory can be provided on the stack or because it noticed that no memory is actually required to produce the correct observable behavior.However, even if the compiler does this optimization, your program still has undefined behavior, first because the destructor is still invoked twice and second because it is unspecified whether or not the compiler performs the optimization. If a program has undefined behavior one cannot expect it to fail reliably. Any behavior is permitted.