views:

167

answers:

2

We once had an interview with a very experienced C++ developer who couldn't answer the following question: is it necessary to call the base class destructor from the derived class destructor in C++?

Obviously the answer is no, C++ will call the base class destructor automagically anyway. But what if we attempt to do the call? As I see it the result will depend on whether the base class destructor can be called twice without invoking erroneous behavior.

For example in this case:

class BaseSafe {
public:
    ~BaseSafe()
    {
    }
private:
    int data;
};

class DerivedSafe {
public:
    ~DerivedSafe()
    {
        BaseSafe::~BaseSafe();
    }
};

everything will be fine - the BaseSafe destructor can be called twice safely and the program will run allright.

But in this case:

class BaseUnsafe {
public:
    BaseUnsafe()
    {
       buffer = new char[100];
    }
    ~BaseUnsafe ()
    {
        delete[] buffer;
    }
private:
    char* buffer;
};

class DerivedUnsafe {
public:
    ~DerivedUnsafe ()
    {
        BaseUnsafe::~BaseUnsafe();
    }
};

the explicic call will run fine, but then the implicit (automagic) call to the destructor will trigger double-delete and undefined behavior.

Looks like it is easy to avoid the UB in the second case. Just set buffer to null pointer after delete[].

But will this help? I mean the destructor is expected to only be run once on a fully constructed object, so the optimizer could decide that setting buffer to null pointer makes no sense and eliminate that code exposing the program to double-delete.

Is the compiler allowed to do that?

+11  A: 

Standard 12.4/14

Once a destructor is invoked for an object, the object no longer exists; the behavior is undefined if the destructor is invoked for an object whose lifetime has ended (3.8).

So I guess the compiler should be free to optimize away the setting of buffer to null since the object no longer exists after calling the destructor.

But even if the setting of the buffer to null wasn't removed by the compiler, it seems like calling the destructor twice would result in UB.

Andreas Brinck
Your interpretation is correct. After the first destructor is called, the objects lifetime ends (3.8) and at the second call 12.4/14 applies. What happens inside the dtor is irrelevant.
MSalters
+3  A: 

Calling the destructor converts an object into raw memory. You cannot destruct raw memory; this is undefined behaviour. The C++ compiler is entitled to do anything it wants. While it is unlikely that it will turn your computer in cottage cheese, it might deliberately trigger a slap-on-the-wrist SEGFAULT (at least in debug mode).

Marcelo Cantos
"The C++ compiler is entitled to do anything it wants." Not really, but it is specifically entitled to do anything you can't detect with a legal program, and there doesn't seem to be a legal way to detect eliding the null assignment. So in theory it might do that. In practice I consider it pretty unlikely. But then the whole use case is pretty unlikely. It's a mistake I made myself on my first day of C++ programming, but never afterwards.
EJP
@EJP: Yes, really! Undefined behaviour means all bets are off. The compiler may cause any behaviour whatsoever and it will still be conformant to the standard, even if it commits crimes against humanity, violates the laws of nature, or goes back in time and kills your grandfather, thus nullifying its own creation (actually, maybe not that last one). Just to prove the point, an early version of gcc would occasionally try to launch emacs playing a game of hanoi towers if it saw a #pragma.
Marcelo Cantos