3

When designing a pure virtual base class (interface) in C++, in what cases would I not want to allow polymorphic deletion via a pointer to my base class?

I've seen questions about why you should make the destructor virtual or not (e.g. this one). They often include guidance that says you should make the destructor public and virtual or protected and non-virtual. However, if I don't know how users will derive from my interface or how they will want to delete objects of the derived class, why wouldn't I always make the destructor virtual in a pure virtual base class?

ToddR
  • 335

4 Answers4

2

Let us precisely look at what you wrote:

If I don't know how users will derive from my interface or how they will want to delete objects of the derived class, why wouldn't I always make the destructor virtual in a pure virtual base class?

If you really don't know how users of a certain class will delete the objects, you will better off to design a class for polymorphic deletion, hence add a virtual base class destructor "just in case". That's explained in the top answer of the question you already mentioned:

.. [having a virtual destructor is] not always necessary, but it ensures the classes in the hierarchy can be used more freely without accidental undefined behaviour if a derived class destructor is invoked using a base class pointer or reference.

However, as a library designer, you may design a library with a virtual class/interface with a more specific contract in mind how it has to be used. You may decide that

  • construction and deletion of concrete instances are completely the responsibility of the user of a lib, and expected to happen in a context where the concrete types are known (hence no polymorphic deletion)

  • the library itself only implements means like the template method pattern, but no deletion of the objects.

For such a case, a pure virtual destructor is probably superfluous and hence negligible, in which case one should make the destructor non-virtual and protected.

For example, think of a program designed with dependency-injection in mind, where several modules are implemented as a class with an interface, for making it possible to mock the class out during unit testing. For these modules, one usually creates only one instance of the class during the live time of the program, and the deletion only happens when the program ends. Why would you need a virtual destructor for such a class? It is perfectly possible to omit any explicit deletion for such a class and rely on the operating system to clean up the processes memory when the program ends.

For a more in-depth discussion about when and how to use virtual destructors, and when not, see Herb Sutter's article from the C++ user's journal from 2001. The article shows also an example of struct unary_function from the old standard libs, which is inheritable, still a virtual destructor isn't needed.

Doc Brown
  • 218,378
1

Unlike other methods, the class of an instance changes while you are running a chain of constructors or destructors. So if a virtual method is called from destructor or constructor it might not be the one you expect, and you need to be extra careful.

gnasher729
  • 49,096
1

The C++ core guidelines indeed recommends

A base class destructor should be either public and virtual, or protected and non-virtual

The reason is that

  1. derived classes may need their own destructor, for example if they have additional non trivial attributes;
  2. the compiler will not know which destructor to call for dynamically created polymorphic objets, and the way to help the compiler to get the right one, is to make the destructor virtual.

The following code (online demo) shows how non-virtual public destructors can screw up, failing to properly destroy the derived class (which means potential leakages, locked resources, etc..):

struct A {              // Class to trace what happens with attributes of the derived class 
    A() { cout<<"    Create attribute class"<<endl; }
    ~A() { cout<<"    Destroy attribute class"<<endl; } 
};
class X {               // Base class 
public: 
    X() { cout<<"  Create base class"<<endl; }
    virtual void test() { cout<<"  Test base class"<<endl; }
    ~X() { cout<<"  Destroy base class"<<endl; } // oops 
};
class XX : public X {   // Derived class
    A a; 
public:
    XX() { cout<<"  Create derived class"<<endl; }
    void test() override { cout<<"  Test derived class"<<endl; }
    ~XX() { cout<<"  Destroy derived class"<<endl; }  
};

void kill_it (X* t) { t->test(); delete t; }

int main() { X *poly;

cout&lt;&lt;&quot;1) Test with base class:&quot;&lt;&lt;endl;
poly = new X(); 
kill_it (poly);

cout&lt;&lt;endl&lt;&lt;&quot;2) Test with derived class&quot;&lt;&lt;endl;
poly = new XX(); 
kill_it (poly);

cout&lt;&lt;endl&lt;&lt;&quot;3) How it should be with derived class&quot;&lt;&lt;endl;
{
    XX dummy_destroyed_when_leaving_the_block;
}

}

For the sake of completeness, you'll easily find out that just making the destructor virtual makes it work correctly (other online demo).

This problem arises also if your design uses smart pointers, unless you think to explicitly provide a deleter for each and every unique_ptr and shared_ptr constructors, which is much more cumbersome and error prone.

You may want to keep the destructor public and non-virtual only in limited situations, for example:

  • your class only encapsulatse related functions without any attributes (seems to be the case with your interface),
  • when no attributes are added to the derived class,
  • no RAII is performed
  • object destruction of derived classes will never ever happen via a pointer to the base class.

But still, it'll remain a maintenance risk (if you need to edit your code in 3 years, will you remember the constraints?) for a little potential (but not even guaranteed) benefit of a few nanoseconds at destruction. If destruction would be so time critical to justify this, you'd better work on specialising allocators than playing with the code safety.

Christophe
  • 81,699
-1

For performance.

Zero-cost abstraction is an important design goal in ะก++. I.e. if a feature is unused, it should cost nothing. As unconditional use of virtual call would incur additional cost, it conflicts with this design goal and is therefore unacceptable.

So use static dispatch for performance, use dynamic dispatch for flexibility and safety.

On a side note, mandatory virtual destructor may be a requirement for one of many "safe" dialects of C++.

Basilevs
  • 3,896