Code crashes when derived class' destructor is virtual and base class' dtor is not
Asked Answered
J

2

6

I tried the following code on gcc 4.4.5.

If the member 'data' is not present, the code executes fine, but in its presence, it crashes. It also doesn't crash when the derived class' dtor is not virtual.

I'm aware that the behavior would be Undefined as listed in C++03 (5.3.5 / 3) in both the cases, but still could someone provide me with some explanation why does it crash in the latter case?

And yes, I know UB means that anything can happen, but still I'd like to know implementation-specific details.

#include<iostream>    
using std::cout;

struct base{
int data;
   base(){
      cout << "ctor of base\n";
   }
   ~base(){
      cout << "dtor of base\n";
   }
};

struct derived :  base{
   derived(){
      cout << "ctor of derived\n";
   }
   virtual ~derived(){
      cout << "dtor of derived\n";
   }
};

int main(){
   base *p = new derived;
   delete p;
}
Jessamyn answered 27/5, 2011 at 16:41 Comment(6)
As this is UB, the only way to "diagnose" this is to take a look at the generated assembly for your particular platform. I'm not sure what is to be gained by doing so!Underlinen
Almost impossible to say - on my g++ 4.5.1 installation, for example it appears to run OK. The whole point about UB is that it is, well UB.Jayme
What benefit will you get from knowing how/why the crash occurs? You won't be able to use that information reliably with any other compiler version; you may not be able to use it with the same compiler version on other platforms.Uninspired
"It seems to work sometimes", but is known to be UB. What was the question?Threonine
I think you should stop moving the mouse. Does it still crash if you unplug it?Neu
@Johannes I remember the exciting days when things WOULD crash if you unplugged the mouse, or just about anything else. Or even dared to touch your network cable :-)Jayme
B
10

Assuming what happens on my system (gcc 4.6.0, linux x86_64) is the same as what happens on yours (it also crashes with data and runs without), the implementation detail is that p does not point at the beginning of the memory block allocated for the object of type derived.

As valgrind told me,

Address 0x595c048 is 8 bytes inside a block of size 16 alloc'd

You can see that for yourself if you print the values of the pointers:

derived * d = new derived;
std::cout << d << '\n';
base *p = d;
std::cout << p << '\n';

And the reason for that is that object layout in gcc is {vtable, base, derived}

When base is empty, the size of {vtable, base, derived} and {base} happen to be the same because allocating an object of empty class occupies nonzero number of bytes, which happens to be equal in both cases.

When derived has no virtual functions, vtable is not present, the addresses are again the same and delete succeeds.

Beebe answered 27/5, 2011 at 16:55 Comment(4)
So, it means that when the derived object is delete-d, the base dtor is called and because the base class doesn't have any information about the vtable, it deletes it as if 'p' refers to the starting address of a base object. Due to the presence of the vtable at 'p' and not the start of base object, there is a crash. Am I correct?Jessamyn
The next question is why having or not a field makes a difference in how the compiler lays out the object. My guess is that it does not change the layout, so it would still be { vptr, [empty base], derived }, but that since there is no data to be pointed the cast from derived to base is processed as a no-op (so well, it points to the vptr... but you are not allowed to dereference it anyway). Does this make any sense?Mordy
@Saurabh Manchanda: When you delete, two things happen: the compiler calls the appropriate destructor (according to your code, which in this case is far from appropriate), and then it releases the memory. To release the memory it uses the type of the most derived destructor that it knows of (in this case base) to obtain the address of the pointer returned by the allocation function, and that pointer is wrong. It is fairly simple to test: int main() { derived *d = new derived; base *b = d; std::cout << d << "," << b << std::endl; delete b; }.Mordy
... what the compiler is doing in the delete is: b->~base(); deallocate(b); (where deallocate is usually free but does not need to be). And that is where it is dying: when you obtained the memory you got the address d, but you are releasing the address b, which was never allocated. Note that even if it would still be undefined behavior, the presence of any virtual method will modify how g++ lays out the class, and it will place base._vptr upfront, and that will cause d == b to pass, and the program not to crash. It is still UB, though.Mordy
A
2

the size of the two types does not match and the layout in your example should differ.

you are comparing pod types versus a type with a vtable (the layout and offsets are implementation defined). when the destructor is called, the address of implicit this is assumed to have the layout of base, but this is actually derived. what's executed is equivalent to writing to/reading from an invalid address.

Areopagite answered 27/5, 2011 at 17:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.