5

I don't understand one thing. For example, I declare class A and class B which is a child of A:

class A {
    public:
        int a;
}

class B : public A {
    public:
        int b;
}

Obviously, if I create instances of A or B, their size in the memory can be determined by the type.

A instanceA; // size of this will probably be the size of int (property a)
B instanceB; // size of this will probably be twice the size of int (properties a and b)

But what if I create dynamic instances and then free them later?

A * instanceAPointer = new A();
A * instanceBPointer = new B();

These are instances of different classes but the program will consider them as instances of class A. That's fine while using them but what about freeing them? To free allocated memory, the program must know the size of memory to free, right?

So if I write

delete instanceAPointer;
delete isntanceBPointer;

How does the program know, how much memory, starting from the address each pointer is pointing to, it should free? Because obviously the objects have different size but the program considers them to be of type A.

Thanks

Michal Artazov
  • 4,368
  • 8
  • 25
  • 38
  • 1
    Maybe this link will help? http://www.openrce.org/articles/files/jangrayhood.pdf – OldProgrammer Jun 06 '16 at 00:51
  • 1
    Actually, it looks like the second one might cause a memory leak, since it's not a polymorphic class. If the class is polymorphic, the compiler is able to deallocate it based on the dynamic type, whether using RTTI or some other method, and automatically free the same amount of memory that was actually allocated. If the class isn't polymorphic, though, I don't believe it's guaranteed to be able to handle that situation properly, so you should always delete it through a pointer of the correct type. – Justin Time - Reinstate Monica Jun 06 '16 at 01:08
  • @JustinTime By polymorphic class you mean class with virtual destructor? – Michal Artazov Jun 06 '16 at 02:08
  • 1
    @Justin: `delete` will never use RTTI. The presence of a virtual destructor takes care of the identification through the vtable. – rubenvb Jun 06 '16 at 07:21
  • @JustinTime Yup, this is why people say always to implement a virtual destructor in any class that a client might delete through a polymorphic pointer, so that the right derived dtor(s) can be looked up and executed. – underscore_d Jun 06 '16 at 11:58
  • I think I get it now. Basically, if I have a polymorphic class, I should declare a virtual destructor. Otherwise, the logic of delete is undefined and might crash depending on the implementation. Right? – Michal Artazov Jun 06 '16 at 14:31
  • @MichalArtazov By polymorphic class, I mean any class that has one or more virtual functions. If a class is intended to be used polymorphically (i.e. in a situation where it's accessed through a pointer to its base class (or one of its base classes, in the case of multiple inheritance)), it should itself be polymorphic, with any functions that might change their behaviour based on the instance's actual type being declared `virtual`. – Justin Time - Reinstate Monica Jun 06 '16 at 21:07
  • And as [underscore_d said](http://stackoverflow.com/questions/37648205/c-dynamic-objects-how-is-object-size-determined-during-runtime?noredirect=1#comment62792434_37648205), if a class is intended to be deleted polymorphically, its destructor should be `virtual`, as well. Declaring a function `virtual` guarantees that the most derived version of the function will always be called, regardless of the type of the pointer it's called from. If a function isn't `virtual`, then calling it through a base class pointer will call that base class' version of the function, which usually isn't intended. – Justin Time - Reinstate Monica Jun 06 '16 at 21:08
  • Function virtuality is inherited: if a function is declared virtual in one class, then all functions declared in any derived class that share the same signature as that function will implicitly be virtual, and override the base class function within the derived class. This means that unless you explicitly specify the base class version, the derived version will always be used, even when called through a base class pointer. – Justin Time - Reinstate Monica Jun 06 '16 at 21:14
  • [Mechanically, this is implemented as a table of function pointers, called a vtable or vftable (for "virtual function table"); `virtual` functions are accessed through the table, instead of being called directly. The compiler ensures that each class' vtable will always store the most-derived version of any `virtual` function that is available to that class, and that the table can be read and parsed correctly by all base class pointers.] – Justin Time - Reinstate Monica Jun 06 '16 at 21:17
  • If you don't use make the destructor virtual, for example, the second `delete` statement will attempt to deallocate the instance using `B::~A()`; it won't know any better, because you gave it an `A*`. If you declare `A::~A()` as virtual, however, then `delete` will be able to call `B::~B()` correctly, even when passed an `A*`; this is because it will actually check `A`'s vtable for the destructor's address (let's call it `A_dtor`). When compiling `A`, the compiler will set `A_dtor` to `&A::~A;`, as you would expect; when compiling `B`, however, it'll set `A_dtor` to `&B::~B;`, so that any – Justin Time - Reinstate Monica Jun 06 '16 at 21:25
  • attempt to call `B::~A()` will instead be redirected to `B::~B()`, preventing any problems. – Justin Time - Reinstate Monica Jun 06 '16 at 21:26
  • [**NOTE:** If present, the vtable is only used when a member function is accessed through a pointer (with `operator->`, as `x->y()`). If the member function is accessed directly (with `operator.`, as `x.y()`), the vtable lookup is skipped and functions are called directly.] – Justin Time - Reinstate Monica Jun 06 '16 at 21:29
  • @rubenvb My bad. Thanks for pointing that out. – Justin Time - Reinstate Monica Jun 06 '16 at 21:30

3 Answers3

7

I am going to assume you know how delete works.

As to how delete knows how to clean up an inherited instance. That's why you use a virtual destructor in inheritance context, otherwise you'll have undefined behavior. Basically, the destructor, like every other virtual function is called via a vtable.

Also recall that: The C++ compiler implicitly destructs the parent class(es) in your destructor

class A {
    public:
        int a;
    virtual ~A(){}
}

class B : public A {
    public:
        int b;
    ~B() { /* The compiler will call ~A() at the very end of this scope */ }
}

That is why this will work;

A* a = new B();
delete a;

By means of vtable, the destructor ~B() will be called by delete. Since the compiler implicitly inserts the destructor call of base class(es) in derived class(es), the destructor of A will be called in ~B().

Community
  • 1
  • 1
WhiZTiM
  • 21,207
  • 4
  • 43
  • 68
  • so, if I don't declare a virtual destructor, the program might or might not crash depending on the compiler implementation? – Michal Artazov Jun 06 '16 at 14:32
3

The behaviour is undefined if you delete an object through a pointer to a base subobject and the class of the subobject does not have a virtual destructor.

On the other hand, if it does have a virtual destructor, then the virtual dispatch mechanism takes care of deallocating the correct amount of memory for the correct address (i.e. that for the complete, most-derived object).

You can discover the address of the most-derived object yourself by applying dynamic_cast<void*> to any appropriate base subobject pointer. (See also this question.)

Community
  • 1
  • 1
Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • 3
    This is correct in regards to undefined behavior, but not correct with regards to memory deallocation. The number off bytes equal to size of real object will always be deallocated, even without virtual destructor. However, proper destructor will not be called, of course. – SergeyA Jun 06 '16 at 01:36
  • @SergeyA: Without the virtual destructor, you don't even find the correct *address* to deallocate, much less the size... – Kerrek SB Jun 06 '16 at 02:03
  • Kerrek, why? You deallocate the whole memory block, the size is known to deallocation routine (usually because it's prefix to the block), and you start from the the address given in the pointer. – SergeyA Jun 06 '16 at 02:05
  • @SergeyA So if I understand it correctly, it doesn't matter what the pointer is pointing to because the referenced memory block implicitly contains information about it's size? – Michal Artazov Jun 06 '16 at 02:11
  • @MichalArtazov, I am having a very technical discussion. This answer (and the one you accepted) are 100% correct when they say about virtual destructor and undefined behavior. It's just that I am not sure about one particular point made by Kerrek, but that's minor, and you should'n focus on it. – SergeyA Jun 06 '16 at 02:21
  • 1
    @SergeyA: You can only deallocate memory via a pointer that was returned from the allocation function. The address of some random subobject isn't in general such a pointer. The size is also important because it may also be passed to the deallocation function (as the second argument), see [expr.delete]/(10.1). – Kerrek SB Jun 06 '16 at 08:27
  • @KerrekSB, agree with second point (about the size). I also see what you mean when you mention random subobjects, but in OP's case (single inheritance, no virtual functions) those two addresses would be the same. – SergeyA Jun 06 '16 at 13:30
  • @SergeyA: Citation needed :-) – Kerrek SB Jun 06 '16 at 13:31
  • @KerrekSB, there wouldn't be a citation, as it is not by any means prescribed. But here is an illustration: http://coliru.stacked-crooked.com/a/f66ed328eb5d5ad4 – SergeyA Jun 06 '16 at 13:36
2

To free allocated memory, the program must know the size of memory to free, right?

If you consider the C library malloc and free, you'll see that there's no need to specify the amount of memory to be freed when calling free, even though in that case free is provided with a void* so has no way to infer it. Instead, allocation libraries typically either record or can infer enough about the memory provided, such that the pointer alone is sufficient to do the deallocation.

This remains true with the C++ deallocation routines: if a base class provides its own static void operator delete(void*, std::size_t) and the base-class destructor is virtual, then it will be passed the size of the dynamic type. By default deallocation ends up at ::operator delete(void*) which won't be given any size: the allocation routines themselves must know enough to operate.

There are a variety of ways allocation routines may work, including:

  • storing the size of an allocation

  • allocating similar-sized objects from a pool of same-sized chunks, such that any pointer into that pool implicitly relates to that chunk size

Tony Delroy
  • 102,968
  • 15
  • 177
  • 252