Destructors and Object Cleanup
A virtual destructor is a destructor declared as virtual in a base class, and it is common in abstract classes.
A Delete That Misses Part of the Object
A destructor is the cleanup step that runs when an object is destroyed. Many programmers first meet destructors as something that runs when an object goes out of scope, and it is easy to conclude that a destructor only needs to care about the class being written at that moment. If a class holds no resources of its own, the destructor can seem unimportant. This belief breaks down as soon as objects are handled through a base class rather than by their exact type.
What do you think happens?
A Container pointer refers to a Vector object. The pointer is deleted, and the Container destructor is not marked virtual. What happens to the memory and resources the Vector owns?
Reveal answer
Only the Container destructor runs, and the Vector's resources can be left unreleased.
Without the virtual keyword, the destruction request stops at the Container destructor. The derived part of the object never gets its chance to clean up what it owns.
Tracing a Delete Through a Base Pointer
Consider an abstract class named Container that defines an interface, and a concrete class Vector that implements that interface. Vector owns memory that it allocates for its elements. In the version below, Container has a destructor that is not marked virtual, and a function destroys a Vector through a Container pointer. The point of this example is to watch which destructors run when that pointer is deleted.
class Container {
public:
~Container() {}
virtual int size() const = 0;
};
class Vector : public Container {
public:
Vector(int n) : data(new int[n]), count(n) {}
~Vector() { delete[] data; }
int size() const override { return count; }
private:
int* data;
int count;
};
void discard(Container* c) {
delete c;
}
int main() {
discard(new Vector(10));
}- 1The Container class defines the interface and has no data of its own.
- 2The Vector class adds a data member that points to heap memory and releases that memory in its destructor.
- 3The discard function is written against Container alone, the way code that holds a base pointer usually is.
- 4When delete c runs, the request is handled using the Container destructor, which is not virtual.
- 5Because the request does not reach Vector's destructor, the array that Vector allocated is not released.
- Container is abstract because it declares a pure virtual function
size(). - Container's destructor is not marked virtual.
- Vector allocates an array in its constructor and releases it in its own destructor.
- discard receives only a Container pointer, so it knows only the interface.
- delete c is the step where destruction is requested through the base pointer.
Read the diagram as a sequence of messages. The caller asks for deletion through the Container pointer. Without the keyword, that request stops at the Container destructor. The Vector part of the object is never given its chance to clean up, so the array it allocated can remain unreleased. The program has no visible error at the point of the delete, which is why this problem is easy to miss.
Why the Base Pointer Cannot Know the Answer
Abstract classes such as Container tend to be manipulated through references or pointers. Code that receives such a pointer usually knows only the interface, not the concrete class behind it. The code that holds the pointer may have been written long before the concrete class existed. When someone destroys a Container through a pointer, that code has no idea what resources the underlying implementation owns.
The problem is therefore one of knowledge. The destruction step must be able to follow the actual type of the object, not the type named in the pointer. Declaring the base destructor virtual makes this possible. The destruction request is dispatched to the destructor of the object the pointer actually refers to, so the derived class can release what it owns.
Adding the virtual Keyword
The only change to the hierarchy is the word virtual placed in front of Container's destructor declaration. Everything else stays the same, including the discard function that deletes through a Container pointer.
class Container {
public:
virtual ~Container() {}
virtual int size() const = 0;
};
class Vector : public Container {
public:
Vector(int n) : data(new int[n]), count(n) {}
~Vector() { delete[] data; }
int size() const override { return count; }
private:
int* data;
int count;
};
void discard(Container* c) {
delete c;
}
int main() {
discard(new Vector(10));
}- 1The virtual keyword marks the Container destructor as one that is dispatched based on the actual object type.
- 2When delete c runs, the request is redirected to Vector's destructor, which releases the array.
- 3After Vector's destructor finishes, the base part of the object is cleaned up through Container's destructor.
- virtual ~
Container()is the only change from the previous version. - discard still deletes through a Container pointer, unchanged.
- Vector's destructor now runs as part of the delete.
The memory layout view explains why the derived destructor matters. The Container pointer covers the base part of the Vector object, but the array that Vector allocated sits outside that base part. Only the Vector destructor knows to release it, which is why the virtual keyword is required for the cleanup to reach that code.
Deciding Which Base Classes Need It
The source ties the need for a virtual destructor to how a base class is used. A base class that acts as an abstract interface is the typical case, because such classes tend to be manipulated through references or pointers. The source notes that this is why a virtual destructor is common in abstract classes. An abstract class like Container does not have a constructor, because it has no data of its own to initialize, but its destructor is still declared virtual.
- The class is used as a base for other classes that own resources, so a derived object may be destroyed through a pointer to the base.
- The class is an abstract interface, so code holding it usually cannot know the concrete type behind the pointer.
- Objects of this hierarchy are manipulated through references or pointers rather than always by their exact type.
- The destruction of a derived object must reach the derived class's cleanup code, which only the virtual keyword guarantees in this setting.
Mistakes Beginners Make with Base Destructors
Common mistake Assuming a destructor only needs to clean up the class currently being written
What it looks likeWriting Container with an empty non-virtual destructor and assuming Vector's array will still be released through a Container pointer.
Why it failsThe destruction request stops at the Container destructor when it is not virtual, so Vector's destructor never runs.
Do this insteadMark the base destructor virtual in any base class that is used through pointers or references.
Common mistake Believing an abstract class with no data needs no destructor
Why it failsAn abstract class like Container can have no data of its own and still be the type through which derived objects are destroyed.
Do this insteadDeclare the destructor virtual even when the abstract base has no constructor and no members.
Common mistake Adding virtual only to the derived class destructor and leaving the base destructor non-virtual, which does not change how a base pointer reaches the derived destructor
Checking Your Understanding of the Delete Path
Suppose a class Shape is an abstract interface with a non-virtual destructor, and a class Circle derives from Shape and allocates a buffer in its own destructor. A function deletes a Shape pointer that refers to a Circle. Trace which destructors run, and state whether the buffer is released.
Hints
- Ask which destructor the delete request is dispatched to when the base destructor is not virtual.
- Look at whether the Circle destructor is reached before the Shape destructor finishes.
- Compare the result with the same code after adding virtual to the Shape destructor.
Key Takeaways
- A base class used as an abstract interface is often manipulated through references or pointers, and code holding such a pointer knows only the interface, not the resources the concrete class owns.
- Without a virtual destructor, deleting a derived object through a base pointer can leave the derived part's resources unreleased, because the destruction request stops at the base destructor.
- Declaring the base destructor virtual lets the destruction request follow the actual object's type, reaching the derived class's cleanup code before the base part is cleaned up.
- A base class meant to be extended through pointers or references should have a virtual destructor, even when it is abstract and has no data or constructor of its own.