How is the size of a polymorphic object deduced in a delete operation?

173 Views Asked by At

I know that there are a lot of similar questions on SO related to what I am about to ask, I've read many of them and still feel a bit vague, so I decided to ask this question.

Given the following code: (which is essentially the same as in this question)

class A {
public:
    int a;
};

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

int main() {
    A* p = new B;
    delete p;
}

I know that deleting p without defining a virtual destructor in A is an undefined behavior specified in the standard and anything could happen thereafter. However, I'm interested in how the size of the polymorphic object on the heap is determined.

I found two sayings on SO which are potentially contradictory: (or I might have misunderstood)

  • We need the virtual destructor to provide the information about the object on the heap for deleting the object through a base class pointer.

    Possible reference.

    In my understanding to such implementation, if we have the virtual destructor, delete p; will first call the destructor of B (and it knows that the first virtual destructor called will correspond to the type of the object that is really on the heap), and the destructor of B knows the size of an object of type B, so the delete operation can extract that information from the first destructor called and use it to free the memory on the heap after the calls of the destructors are done.

    If this is what happens behind the scenes, then delete p; in the above code will not be able to free the heap memory occupied by the int b, because in the above case the first (and the only) destructor called is the destructor of A, and there is no information about int b to be extracted for the delete operation.

  • The heap can "automatically" deduce the size of the polymorphic object.

    Possible references: 1, 2.

    If this is what happens behind the scenes, then delete p; in the above code will be able to free the heap memory occupied by the int b. And in this case delete p; "accidentally" works properly since object of B does not acquire resources from "elsewhere" (e.g. no data members of pointer).

May I ask which one of the two understandings above is correct?

And if the heap can deduce the information about the polymorphic object, may I ask how exactly the heap does that?

3

There are 3 best solutions below

5
Remy Lebeau On

new implementations store metadata about the memory they allocate so that delete knows how to free the memory correctly. The size of the object is not calculated by delete, because new already knew the size up front.

Calling destructors is handled via normal polymorphic dispatch. That is why calling delete on a base pointer without a virtual destructor is undefined behavior.

Freeing the allocated memory is a separate operation afterwards, and doesn't need to depend on the type of the class that resides in the memory (unless the class overloads operator delete, that is).

0
john On

I'm not sure either is correct (of a typical implementation). Destroying an object and deallocating memory are two separate processes, and only the later needs to know the size of the object, or more strictly the size of the block of allocated memory, which may be larger than the size of the object. A common way to do this is to store the size of the block of allocated memory in the bytes immediately preceding the pointer itself.

Also consider that the C++ heap would commonly be implemented by using the C heap API, which has no knowledge of destructors etc.

0
Jeff Garrett On

This is part of the ABI.

On the Itanium ABI, the one typically used on Linux, the vtable records two entries for a virtual destructor. One entry calls the destructors and but not delete. This is the complete object destructor. One entry calls the destructors and also delete. This is the deleting destructor. See here.

Since the deleting destructor is virtual and type-specific, it obviously has the size of the complete object.

The other answers get a little mixed up about this. The destruction and the deletion are two logical steps, but it is common to provide a combined form because the requirements are hard to satisfy otherwise. new does not and cannot store size information in the object allocation itself (for non-array cases). And that size is necessary because sized operator delete has preference. And the implementation cannot rely on a particular behavior because these functions are globally replaceable, so a different TU could define them.