C++ Base Class Destructors

Introduction

In terms of the interface design in C++, sometimes it could be confusing to the developer as there can be many options such as runtime polymorphisms and different key words and specifiers such as public, protected, private, and virtual.

For the C++ base class, the rule of thumb for destructor is “A base class destructor should be either public and virtual, or protected and non-virtual”. There are also some very rare use cases when the base class destructor is protected and virtual.

In this blog post, I would like to discuss when and why to use public and virtual destructor, protected and non-virtual destructor, protected and virtual destructor for base class, and how the other combinations are eliminated.

Public and Virtual Destructor

When a base class is meant to be polymorphic at runtime, we should use public and virtual destructor for the base class, to ensure the base class always gets destructured correctly.

public_virtual_destructor.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <memory>

class Base
{
public:
Base() {}
virtual ~Base() { std::cout << "Base::~Base() called" << std::endl; }
};

class Derived : public Base
{
public:
Derived() {}
~Derived() { std::cout << "Derived::~Derived() called" << std::endl; }
};

int main()
{
std::unique_ptr<Base> ptr_base_1{std::make_unique<Base>()};
std::unique_ptr<Base> ptr_base_2{std::make_unique<Derived>()};
std::unique_ptr<Derived> ptr_derived{std::make_unique<Derived>()};
}

If the base class destructor is not public, the base class destructor is not accessible outside the class and the code of deleting the object from a base class pointer will result in a compile-time error.

If the base class destructor is not virtual, when the object is deleted via a base class pointer, only the base class instance will be deleted and the derived class instance, if there is any, will become a memory leak.

Protected and Non-Virtual Destructor

When a base class is not meant to be polymorphic at runtime, but its interface is still meant to be inherited, we should use protected and non-virtual destructor for the base class.

protected_non_virtual_destructor.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <memory>

class Base
{
public:
Base() {}

protected:
~Base() { std::cout << "Base::~Base() called" << std::endl; }
};

class Derived : public Base
{
public:
Derived() {}
~Derived() { std::cout << "Derived::~Derived() called" << std::endl; }
};

int main()
{
// This cannot compile because Base::~Base() is protected.
// std::unique_ptr<Base> ptr_base_1{std::make_unique<Base>()};
// This cannot compile because Base::~Base() is protected.
// std::unique_ptr<Base> ptr_base_2{std::make_unique<Derived>()};
std::unique_ptr<Derived> ptr_derived{std::make_unique<Derived>()};
}

If a base class is not meant to be polymorphic at runtime, the base class is not meant to be an abstract class which has at least one virtual function to be overridden. Therefore, for performance reasons, there should be no virtual function in the base class, including the destructor. The user could still use virtual functions for the base class, including making the destructor virtual, and the program still compiles and runs fine. But having abstract classes and virtual functions results in a vtable attached to any class instance created at runtime and these runtime polymorphism performance overhead can be large, especially when there are many of such objects being created and deleted.

The base class destructor cannot be private. Otherwise, the base class cannot be destructed after the inherited class is destructed and it will be a compile-time error.

The base class destructor is not recommended be public. The user could accidentally use the base class pointer to delete derived object and it is an undefined behavior.

Protected and Virtual Destructor

As I mentioned in the “Protected and Non-Virtual Destructor” previously, making the protected destructor virtual will not break the program but it comes with a performance cost. Therefore, usually we will not make the protected destructor virtual and pay unnecessary performance costs. However, there are some corner cases that we will have to make the protected destructor virtual and pay the performance costs.

This is an example of a simplified Component Object Model (COM) object implementation and derivation. In this case, we have to make the base class protected destructor virtual to make sure both the derived object and the base object are correctly destructed.

protected_virtual_destructor.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <memory>

// Not thread-safe though.
class Base
{
public:
Base(){};

int add_ref() { return ++m_cref; }

int release()
{
--m_cref;
if (0 == m_cref)
{
delete this;
}
return m_cref;
}

protected:
virtual ~Base() { std::cout << "Base::~Base() called" << std::endl; }
int m_cref{1};
};

class Derived : public Base
{
public:
Derived(){};

protected:
~Derived() { std::cout << "Derived::~Derived() called" << std::endl; }
};

int main()
{
// In these cases, we use no RAII.

Base* ptr_base = new Derived{};
// This cannot compile because Base::~Base() is protected.
// delete ptr_base;
ptr_base->release();

Derived* ptr_derived = new Derived{};
// This cannot compile because Derived::~Derived() is protected.
// delete ptr_derived;
ptr_derived->release();

return 0;
}

References

Author

Lei Mao

Posted on

04-05-2023

Updated on

04-05-2023

Licensed under


Comments