Do I need a virtual destructor for a second interface class?
Asked Answered
G

2

18

I've got classes called "Base" and "Derived".

struct Base {
    Base() = default;
    virtual ~Base() = default;
    Base(const Base&) = delete;
    Base& operator=(const Base&) = delete;

    virtual void DoStuff() = 0;
};

The "Base" class needs virtual destructor and it's understandable. Also I don't allow copying of this class

struct Derived : Base {
    Derived() = default;
    ~Derived() override = default;

    void DoStuff() override { /*...*/ }
};
int main()
{
    std::shared_ptr<Base> a = std::make_shared<Derived>();
    a->DoStuff();
    return 0;
}

Now let's introduce other classes called, I don't know, Callable and DerivedCallable

struct Callable
{
    virtual void Call() = 0;
};
struct DerivedCallable : Base, Callable
{
    DerivedCallable() = default;
    ~DerivedCallable() override = default;

    void DoStuff() override { /*...*/ }
    void Call() override { /*...*/ }
};
int main()
{
    std::shared_ptr<Base> a = std::make_shared<Derived>();
    a->DoStuff();

    {
        auto callableA = std::dynamic_pointer_cast<DerivedCallable>(a);
        if(callableA) {
            callableA->Call();
        }
    }

    std::shared_ptr<Base> b = std::make_shared<DerivedCallable>();
    b->DoStuff();
    
    {
        auto callableB = std::dynamic_pointer_cast<DerivedCallable>(b);
        if(callableB) {
            callableB->Call();
        }
    }

    return 0;
}

Derived does not inherit from Callable, so callableA is nullptr, thus the if statement won't execute the Call() function.

DerivedCallable on the other hand inherits from Callable and the std::dynamic_pointer_cast will increase the ref count of the object to 2, so when the callableB gets out of the scope the object won't be deallocated, only the refcount will decrease to 1 and then the main function will deallocate b.

Does Callable need to have a virtual destructor?

Gendron answered 1/2, 2023 at 14:34 Comment(3)
See for yourself: en.cppreference.com/w/cpp/types/is_polymorphic. Use that on your type. Good question by the way, +1Religion
@Edziju What are that Base& operator(const Base&) = delete; and Derived() = default; Derived() override = default;?Gregggreggory
My bad, should be operator= and ~Derived() override = default.Gendron
H
19

You only need a virtual destructor if you are ever going to delete a derived class object through a base class pointer.

Since you are using std::shared_ptr, you don't need any virtual destructors because the shared_ptr stores a correctly typed deleter (no matter how you cast it).

If you are ever going to own a DerivedCallable object with a Callable pointer (std::unique_ptr<Callable> or something else that calls delete on a Callable*), then it should have a virtual destructor. However, if you only ever have non-owning references with Callable*, then you don't strictly need the virtual destructor.

Adding a virtual destructor when the class already has other virtual members is very cheap, so it's fine to add it so that you don't have to worry about accidentally delete-ing it wrong.

Harvestman answered 1/2, 2023 at 14:53 Comment(7)
That's what I thought. Just needed to make sure :D. ThanksGendron
@Edziju • In this situation of having a pointer that should not be used to delete the object, I put in a protected: virtual ~Foo() = default;. Technically, the virtual should not be needed, but it helps to squelch irrelevant compiler warnings.Heuristic
There is an exception form shared_ptr holding proper deleter when raw pointer is converted to shared_ptr. It is better to have virtual destructor if any other member function is virtual.Favored
@MarekR A raw pointer of a derived type converted to a shared_ptr of the base type (like std::shared_ptr<Base>(new Derived)) correctly stores the derived type's destructor. If it's cast to the base type as a raw pointer, then you have an owning pointer of the base class type and you would need a virtual destructor. I do agree that having a virtual destructor anyways is good (see the last point)Harvestman
@Harvestman I mean more complex example when raw pointer points to base class. In such case it will be unable to detect proper dtor. It is extreame corner case but it can happen. Also it is better to have class which properly works with std::unique_ptr .Favored
hmmm are you sure about that? Callable got a pure virtual methodGeralyngeraniaceous
"Since you are using std::shared_ptr, you don't need any virtual destructors because the shared_ptr stores a correctly typed deleter (no matter how you cast it)." that isn't quite true (just 90% true). It is make shared that makes this safe, and/or initial construction through a pointer to a type that can be safely deleted, not shared ptr itself. Mentioning this would improve the answer (outside of comments)Catholicize
U
7

It depends. In theory, Base doesn't need a virtual destructor. You need a destructor to be virtual when you lug around an object which has a different dynamic type than its static type.

In your example, you have Base pointer that really points to a Derivied object. If you were not to make ~Base() virtual, then, destroying that object would exhibit undefined behaviour -- likely by failing to destroy the Derived portion of the object.

So, as long as you don't plan to have an (owning!) pointer to your object through a particular base class, that base class's destructor need not be virtual.

Unlay answered 1/2, 2023 at 14:54 Comment(2)
"fail to destroy the Derived portion of the object.". It would be UB instead, even if, in practice, chances are that it is that behavior.Glick
That's what I thought. Just needed to make sure :D. ThanksGendron

© 2022 - 2024 — McMap. All rights reserved.