c++g++polymorphism

How does C++ select the `delete` operator in case of replacement in subclass?


Let's assume I have the following code (https://godbolt.org/z/MW4ETf7a8):

X.h

#include <iostream>

struct X{
    void* operator new(std::size_t size)
    {
        std::cout << "new X\n";
        return malloc(size);
    }

    void operator delete(void* ptr)
    {
        std::cout << "delete X\n";
        return free(ptr);
    }

    virtual ~X() = default;
};

struct ArenaAllocatedX : public X {
    void* operator new(std::size_t size)
    {
        std::cout << "new ArenaAllocatedX\n";
        return malloc(size);
    }

    void operator delete(void* ptr)
    {
        std::cout << "delete ArenaAllocatedX\n";
        return free(ptr);
    }
};

main.cpp

int main() {
    X* x1 = new X();
    delete x1;

    X* x2 = new ArenaAllocatedX ();
    delete x2;    
}

Using GCC 10.3.1, x1 calls the new and delete operators of class X, while x2 calls of class ArenaAllocatedX.

I understand how it will pick up the new operator, as it has the actual class name to its right, but I don't understand how the delete operator is picked for the subclass while being pointed to by X*.

Are the new/delete operators considered functions inside the virtual table (I dumped the VTable using gcc -f-dump-lang-class and yet I don't see any delete operators)?

The VTable dumps:

Vtable for X
X::_ZTV1X: 4 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1X)
16    (int (*)(...))X::~X
24    (int (*)(...))X::~X

Class X
   size=8 align=8
   base size=8 base align=8
X (0x0x7f38cd807780) 0 nearly-empty
    vptr=((& X::_ZTV1X) + 16)

Vtable for ArenaAllocatedX
ArenaAllocatedX::_ZTV15ArenaAllocatedX: 4 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI15ArenaAllocatedX)
16    (int (*)(...))ArenaAllocatedX::~ArenaAllocatedX
24    (int (*)(...))ArenaAllocatedX::~ArenaAllocatedX

Class ArenaAllocatedX
   size=8 align=8
   base size=8 base align=8
ArenaAllocatedX (0x0x7f38cd848680) 0 nearly-empty
    vptr=((& ArenaAllocatedX::_ZTV15ArenaAllocatedX) + 16)
X (0x0x7f38cd807cc0) 0 nearly-empty
      primary-for ArenaAllocatedX (0x0x7f38cd848680)

How does C++ pick which delete operator to use in the code provided?


Solution

  • C++ specifies that in a single-object (rather than array) delete expression, if the provided pointer points to a type with virtual destructor, operator delete will be called on the most-derived object of the object that the pointer points to. Lookup for operator delete is also performed in the class scope of that most-derived type, not in the class scope of the operand's static type.

    This is only true if the operand to delete's static type is a pointer to a type with virtual destructor. Otherwise passing a base class subobject's pointer to delete will have undefined behavior. Forgetting to add the virtual destructor for a type that is used polymorphic in its base class is a common mistake to make.

    So, this formally explains the behavior you observe. As for how the compiler achieves this behavior:


    As you can see in your vtable, there are two destructors for each class.

    If you have a class with a virtual destructor, then the compiler will emit two instances of the destructor.

    One is the normal destructor that performs the same operations that a non-virtual destructor would and that would be used if you explicitly called the destructor.

    The second one is the so-called deleting destructor. It also performs the usual destructor actions (or calls the first implementation), but will additionally at the end call the operator delete of the class to which the destructor belongs.

    The destructor itself is virtual and both destructor implementations can be found for the most-derived object by virtual lookup. For a delete expression the compiler will then call the deleting destructor by virtual dispatch. Because the operator delete call is embedded into that destructor, the compiler will not need to add any additional operator delete call at the site of the delete expression.

    The "usual" destructor is then also called the base object destructor, because it will be used in case of base class subobjects (if any), which do not themselves have an associated allocation.

    Because of this special implementation, operator delete lookup rules are different with virtual destructors as well and template instantiation rules are different. operator delete will be looked up at the point of definition of the virtual destructor, not at the point where the delete expression appears. This may also lead to confusing results.