3
votes

There is already a question asking about the "real-world" behavior of deleteing a pointer to a base class that lacks a virtual destructor, but the question is restricted to a very limited case (the derived class has no members with non-trivial destructors), and the accepted answer just says there's no way to know without checking the behavior of every compiler.

....but that isn't actually very helpful; knowing that every compiler might behave differently doesn't tell us anything about the behavior of any particular compiler. So, what do Clang and G++ do in this case? I would assume they would simply call the base-class destructor, then deallocate the memory (for the entire derived class). Is this the case?

Or, if it's not possible to determine this for all versions of GCC and Clang, how about GCC 4.9 and 5.1, and Clang 3.5 through 3.7?

2
What would be the point of figuring this out? It's undefined behavior, and for all you know, the behavior could change the next time you recompile, or change the order of data members, or add a new one, or just about do anything ...Praetorian
@Praetorian: that's true if you simply observe the behaviour, but if you analyse the code carefully you might be able to make a more conclusive statement relevant to a specific version of the compiler. But then the next release could be completely different, and who wants an application that might break with any compiler release or patch, let alone port? Anyway, not sure why Kyle would expect anyone here to do the research for him....Tony Delroy
@KemyLand: why do you want to know this? There are sound reasons - if you've released code with such a bug and want to know whether to rush out a patch or just fix it for the next release, but there are poor and outright bogus reasons too that people here might be able to shed light on.Tony Delroy
@Praetorian Suppose I have a program that seems to run reliably, but upon inspecting the code I notice a subtle case of UB. If I knew that a memory leak was the worst thing that could happen in the particular scenario, I'd probably just continue to run the program as needed. But "anything can happen" implies that even running a program you've run before without any noticeable ill effect might destroy your hard drive.Kyle Strand
The quest here is noble - I've seen many SO questions get answered because a certain cause of UB has a familiar smell that leads good programmers to the source of the problem. But what makes this question not work here, I think, is that for even one version of one compiler, an UB is not typically tested for consistency. Someone would need to prove that your delete scenario is the same for all build options, and CPU types, and optimization levels, and so on.Drew Dormann

2 Answers

3
votes

First, the standard disclaimer: this is undefined behavior, so even with one specific compiler, changing the compiler flags, the day of the week, or the way you look at the computer could change the behavior.

The following all assumes you have some sort of at least slightly non-trivial destruction happening in your destructors (e.g., the objects delete some memory, or contain object others that themselves delete some memory).

In the simple case (single inheritance) you typically get something roughly equivalent to static binding--that is, if you destroy a derived object via a pointer to a base object, only the base constructor is invoked so the object isn't destroyed properly.

If you use multiple inheritance, and you destroy an object of derived class via the "first" base class, it'll typically be about the same as if you used single inheritance--the base class destructor will be invoked, but the derived class destructor won't be.

If you have multiple inheritance and destroy a derived object via a pointer to the second (or subsequent) base class, your program will typically crash. With multiple inheritance, you have multiple base class objects at multiple offsets in the derived object.

enter image description here

In the typical case, the first base class will be at the beginning of the derived object, so using the address of derived as a pointer to the first base class object works about the same as in the single inheritance case--we get the equivalent of static binding/static dispatch.

If we try this with any of the other base classes, a pointer to the derived doesn't point to an object of that base class. The pointer needs to be adjusted to point to the second (or subsequent) base class before it can be used as a pointer to that type of object at all.

With a non-virtual destructor, what'll typically happen is that the code will basically take that address of that first base class object, do roughly the equivalent of a reinterpret_cast on it, and try to use that memory as if it were an object of the base class specified by the pointer (e.g., base2). For example, let's assume base2 has a pointer at offset 14, and base2's destructor attempts to delete a block of memory it points at. With a non-virtual destructor, it'll probably receive a pointer to the base1 subject--but it'll still look at offset 14 from there, and try to treat that as a pointer, and pass it to delete. It could be that base1 contains a pointer at that offset, and it's actually pointing at some dynamically allocated memory, in which case this might actually appear to succeed. Then again, it could also be that it's something entirely different, and the program dies with an error message about (for example) attempting to free an invalid pointer.

It's also possible that base1 is smaller that 14 bytes in size, so this ends up actually manipulating (say) offset 4 in base2.

Bottom line: for a case like this, things get really ugly in a hurry. The very best you can hope for is that the program dies quickly and loudly.

Just for kicks, quick demo code:

#include <iostream>
#include <string>
#include <vector>

class base{ 
    char *data;
    std::string s;
    std::vector<int> v;
public:
    base() { data = new char;  v.push_back(1); s.push_back('a'); }
    ~base() { std::cout << "~base\n"; delete data; }
};

class base2 {
    char *data2;
public:
    base2() : data2(new char) {}
    ~base2() { std::cout << "~base2\n"; delete data2; }
};

class derived : public base, public base2 { 
    char *more_data;

public:
    derived() : more_data(new char) {}
    ~derived() { std::cout << "~derived\n"; delete more_data; }
};

int main() {
    base2 *b = new derived;
    delete b;
}

g++/Linux: Segmentation fault
clang/Linux: Segmentation fault
VC++/Windows: Popup: "foo.exe has stopped working" "A problem caused the program to stop working correctly. Please close the program."

If we change the pointer to base instead of base2, we get ~base from all the compilers (and if we derive only from one base class, and use a pointer to that base class, we get the same: only that base class' destructor runs).

-6
votes

If you delete an object without a virtual destructor, the compiler will probably assume that the deleted address is the address of the most derived object.

Unless you use a primary base class to delete the object, this won't be the case, so the compiler will call operator delete with an incorrect address.

Of course the compiler will not call the destructor of the derived class, or operator delete of the derived class (if there is one).