skip to Main Content

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


  1. 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:

    [Note 3: The deallocation function is called regardless of whether the destructor for the object or some element of the array throws an exception.
    — end note]

    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 using myobject->~MyClass(), catch any exceptions, then free() the memory. Then I would also replace new with malloc + placement new (for symmetry, and because the wording suggests new/delete allocations can be elided).

    Login or Signup to reply.
  2. delete and delete[] 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 and free 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search