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.
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).