Search code examples
c++destructordelete-operatorvirtual-destructor

deleting object through pointer to base without virtual destructor


I have the code:

class A1 {
public:
    A1() { cout << "A1"; }
    virtual ~A1() { cout << "~A1"; }
};

class A2 {
public:
    A2() { cout << "A2"; }
    ~A2() { cout << "~A2"; }
};

class B : public A1, A2 {
public:
    B() { cout << "B"; }
    ~B() { cout << "~B"; }
};

int main() {
    A1* pa1 = new B;
    delete pa1;

    return 0;
}

It works totally fine because I use a virtual destructor. Output: A1A2B~B~A2~A1 as expected. Then I remove virtual:

class A1 {
public:
    A1() { cout << "A1"; }
    ~A1() { cout << "~A1"; }
};

class A2 {
public:
    A2() { cout << "A2"; }
    ~A2() { cout << "~A2"; }
};

class B : public A1, A2 {
public:
    B() { cout << "B"; }
    ~B() { cout << "~B"; }
};

int main() {
    A1* pa1 = new B;
    delete pa1;

    return 0;
}

And it outputs this: A1A2B~A1, which I don't understand. I thought it was supposed to output A1A2B since ~B() isn't going to run, and we don't have an instance of A1, so ~A1() should not be invoked as well. And then, why only ~A1() is running, without ~A2()? Why does it behave like this?


Solution

  • Without virtual the destructor is resolved statically, based on the declared type of the pointer. The compiler sees an A1 * so delete pa1 will call the ~A1 destructor, and nothing else.

    we don't have an instance of A1, so ~A1() should not be invoked as well.

    B is derived from A1 so an instance of A1 exists as a subobject of every B. Otherwise A1* pa1 = new B; would not even compile.

    And then, why only ~A1() is running, without ~A2()? Why does it behave like this?

    Because the pointer is declared as an A1 *. That the actual object pointed to is a B does not matter since the destructor is not virtual.

    The same way, if you changed the A2 inheritance to public and declared A2 *pa2 = new B; then delete pa2 would call ~A2 and nothing else.


    [ EDIT ] The above attempts to answer the direct question of how the compiler resolves the destructor call. An important caveat, however, is that deleting a pointer to a derived type through the base pointer is technically UB (undefined behavior) when the destructor of the base class is not virtual. Quoting from the C++ standard 5.3.5/3:

    In the first alternative (delete object), if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined.

    In practice, many implementations will allow non-virtual destructors to still "work" as expected in certain simple cases, such as single inheritance between non-polymorphic types. Might even work for pointers to the first base class in a multiple inheritance case (for example, the pa1 above). But it's still UB strictly speaking, and there are cases where it's almost guaranteed to fail, such as pointers to a base class other than the first one (see an example of one such error here).