16

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?

8
  • The new and delete operators are not virtual, so there is no need for them to be in the vtable. I don't think they would make much sense virtual, but I've never had cause to try it. Commented Jun 24 at 21:31
  • operators new/delete are static even if they are not declared such
    – Gene
    Commented Jun 24 at 21:32
  • 1
    In the interests of ensuring this question and answer(s) reach people to whom they are useful, I suggest tagging the question as g++ or similar. The details you describe about the vtable are specific to (versions of) that compiler, and any answers will also be specific to that compiler. There is no requirement in Standard C++ for there to even be a vtable, or that other implementations work the same way as g++ (and other implementations do implement things differently than g++).
    – Peter
    Commented Jun 25 at 5:36
  • 1
    The standard may not specify a vtable, but it does specify the behavior of virtual functions. That especially includes destructors. Commented Jun 25 at 21:18
  • 2
    Well, yeah, the standard specifies the required observable behaviour. It doesn't specify how that behaviour is achieved. And the details you are asking for comments on are specifics related to how one compiler implements the required behaviour. Which brings us back to my original suggestion of using the g++ tag because your question is specific to that compiler.
    – Peter
    Commented Jun 26 at 1:26

1 Answer 1

21

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.

2
  • 1
    perhaps worth mentioning that this answers "How does g++ pick which delete operator to use in the code provided?" rather than "How does c++ ...". Though this just due to the question assuming what they see in their implementation is what c++ mandates. Commented Jun 25 at 7:20
  • 2
    @463035818_is_not_an_ai I took OP to be asking about the implementation rather than wondering about the behavior itself. But I added a short explanation at the beginning. Commented Jun 25 at 18:24

Not the answer you're looking for? Browse other questions tagged or ask your own question.