Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Virtual Special Member Functions
Ensure proper cleanup in hierarchies with virtual destructors.
Virtual Destructors, Virtual Assignment, and Overriding Virtualization
Virtual destructors
Although C++ provides a default destructor for your classes if you do not provide one yourself, it is sometimes the case that you will want to provide your own destructor (particularly if the class needs to deallocate memory). You should always make your destructors virtual if you're dealing with inheritance. Consider the following example:
#include <iostream>
class Resource
{
public:
~Resource() // note: not virtual
{
std::cout << "Calling ~Resource()\n";
}
};
class FileResource: public Resource
{
private:
int* m_buffer{};
public:
FileResource(int size)
: m_buffer{ new int[size] }
{
}
~FileResource() // note: not virtual (your compiler may warn you about this)
{
std::cout << "Calling ~FileResource()\n";
delete[] m_buffer;
}
};
int main()
{
FileResource* fileRes{ new FileResource(10) };
Resource* res{ fileRes };
delete res;
return 0;
}
Note: If you compile the above example, your compiler may warn you about the non-virtual destructor (which is intentional for this example). You may need to disable the compiler flag that treats warnings as errors to proceed.
Because res is a Resource pointer, when res is deleted, the program looks to see if the Resource destructor is virtual. It's not, so it assumes it only needs to call the Resource destructor. We can see this in the fact that the above example prints:
Calling ~Resource()
However, we really want the delete function to call FileResource's destructor (which will call Resource's destructor in turn), otherwise m_buffer will not be deleted. We do this by making Resource's destructor virtual:
#include <iostream>
class Resource
{
public:
virtual ~Resource() // note: virtual
{
std::cout << "Calling ~Resource()\n";
}
};
class FileResource: public Resource
{
private:
int* m_buffer{};
public:
FileResource(int size)
: m_buffer{ new int[size] }
{
}
virtual ~FileResource() // note: virtual
{
std::cout << "Calling ~FileResource()\n";
delete[] m_buffer;
}
};
int main()
{
FileResource* fileRes{ new FileResource(10) };
Resource* res{ fileRes };
delete res;
return 0;
}
Now this program produces the following result:
Calling ~FileResource() Calling ~Resource()
Whenever you are dealing with inheritance, you should make any explicit destructors virtual. As with normal virtual member functions, if a base class function is virtual, all derived overrides will be considered virtual regardless of whether they are specified as such. It is not necessary to create an empty derived class destructor just to mark it as virtual. Note that if you want your base class to have a virtual destructor that is otherwise empty, you can define your destructor this way:
virtual ~Resource() = default; // generate a virtual default destructor
Virtual assignment
It is possible to make the assignment operator virtual. However, unlike the destructor case where virtualization is always a good idea, virtualizing the assignment operator really opens up a bag full of worms and gets into some advanced topics outside of the scope of this tutorial. Consequently, we are going to recommend you leave your assignments non-virtual for now, in the interest of simplicity.
Ignoring virtualization
Very rarely you may want to ignore the virtualization of a function. For example, consider the following code:
#include <string_view>
class Resource
{
public:
virtual ~Resource() = default;
virtual std::string_view getType() const { return "Resource"; }
};
class FileResource: public Resource
{
public:
virtual std::string_view getType() const { return "FileResource"; }
};
There may be cases where you want a Resource pointer to a FileResource object to call Resource::getType() instead of FileResource::getType(). To do so, simply use the scope resolution operator:
#include <iostream>
int main()
{
FileResource fileRes{};
const Resource& res{ fileRes };
// Calls Resource::getType() instead of the virtualized FileResource::getType()
std::cout << res.Resource::getType() << '\n';
return 0;
}
You probably won't use this very often, but it's good to know it's at least possible.
Should we make all destructors virtual?
This is a common question asked by new programmers. As noted in the top example, if the base class destructor isn't marked as virtual, then the program is at risk for leaking memory if a programmer later deletes a base class pointer that is pointing to a derived object. One way to avoid this is to mark all your destructors as virtual. But should you?
It's easy to say yes, so that way you can later use any class as a base class -- but there's a performance penalty for doing so (a virtual pointer added to every instance of your class). So you have to balance that cost, as well as your intent.
We'd suggest the following: If a class isn't explicitly designed to be a base class, then it's generally better to have no virtual members and no virtual destructor. The class can still be used via composition. If a class is designed to be used as a base class and/or has any virtual functions, then it should always have a virtual destructor.
If the decision is made to have a class not be inheritable, then the next question is whether it's possible to enforce this.
Conventional wisdom (as initially put forth by Herb Sutter, a highly regarded C++ guru) has suggested avoiding the non-virtual destructor memory leak situation as follows, "A base class destructor should be either public and virtual, or protected and non-virtual." A base class with a protected destructor can't be deleted using a base class pointer, which prevents deleting a derived class object through a base class pointer.
Unfortunately, this also prevents any use of the base class destructor by the public. That means:
- We shouldn't dynamically allocate base class objects by we have no conventional way to delete them (there are non-conventional workarounds, but yuck).
- We can't even statically allocate base class objects because the destructor isn't accessible when they go out of scope.
In other words, using this method, to make the derived class safe, we have to make the base class practically unusable by itself.
Now that the final specifier has been introduced into the language, our recommendations are as follows:
- If you intend your class to be inherited from, make sure your destructor is virtual and public.
- If you do not intend your class to be inherited from, mark your class as final. This will prevent other classes from inheriting from it in the first place, without imposing any other use restrictions on the class itself.
Summary
Virtual destructors: When dealing with inheritance, always make explicit destructors virtual in base classes. Without a virtual destructor, deleting a derived class object through a base class pointer only calls the base destructor, causing memory leaks and undefined behavior. Making the base destructor virtual ensures the derived destructor is called first, followed by the base destructor.
Automatic virtual in derived classes: If a base class destructor is virtual, all derived class destructors are automatically virtual, regardless of whether they're explicitly marked as such. You don't need to create an empty destructor in derived classes just to mark it virtual. For an empty virtual destructor, use virtual ~ClassName() = default;.
Virtual assignment: While technically possible to make the assignment operator virtual, this opens complex issues beyond basic C++ programming. The recommendation is to leave assignment operators non-virtual in the interest of simplicity and avoiding advanced complications.
Ignoring virtualization: To explicitly call the base class version of a virtual function (ignoring virtual resolution), use the scope resolution operator: basePointer.BaseClass::function(). This bypasses virtual function resolution and directly calls the specified class version, though it's rarely needed in practice.
Protected destructor approach: Conventional wisdom suggested making base class destructors either public and virtual, or protected and non-virtual (to prevent deletion through base pointers). However, protected destructors make the base class nearly unusable on its own, as you cannot create or properly destroy base class objects.
Modern recommendation with final: If you intend a class to be inherited from, make its destructor virtual and public. If you don't intend inheritance, mark the class as final, which prevents inheritance without imposing other restrictions. This provides clear design intent and prevents misuse without the limitations of protected destructors.
Virtual destructors are essential for proper cleanup in inheritance hierarchies, ensuring derived class resources are properly released when objects are deleted through base class pointers, preventing memory leaks and ensuring correct program behavior.
Virtual Special Member Functions - Quiz
Test your understanding of the lesson.
Practice Exercises
Virtual Destructors
Implement virtual destructors to ensure proper cleanup of derived classes when deleting through base class pointers. Understand the importance of virtual destructors in polymorphic hierarchies.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!