5. Destructors

While constructors are responsible for various situations where an object is created, C++ also offers a way to handle object destruction. C++ doesn’t provide any form of garbage collection available in many popular programming languages, but thanks to precise lifetime specification, you can be confident when your object will be destroyed.

Each class has a special member function called a destructor. If you don’t write one, the compiler prepares a default implementation. A destructor is called when an object ends its lifetime. In most cases, it means that an object goes out of the scope (for stack-allocated variables), or when a delete operator is called (for heap-allocated variables). Additionally, when you have a user-defined class, it will automatically call destructors for its data members. For more information about lifetime, see a good summary at C++Reference page.

Basics

Before we move on, it would be good to expand our terminology. So far I mentioned “object” to refer to entities of some type and relied on our “intuition” on how to access such entities. But the C++ Standard defines an object in the following terms (simplified, based on C++ Draft - intro.object):

And continuing:

Here’s a basic scenario for a destructor that handles a case where the lifetime of an object ends:

Ex 5.1. A logging destructor. Run @Compiler Explorer
#include <iostream>
#include <string>

class Product {
public:    
    explicit Product(const char* name, unsigned id)
    : name_(name)
    , id_(id)
    { 
        std::cout << name << ", id " << id << '\n';
    }

    ~Product() {
        std::cout << name_ << " destructor...\n";
    }

    std::string Name() const { return name_; }
    unsigned Id() const { return id_; }

private:
    std::string name_; 
    unsigned id_; 
};

The example contains the following special member function:

~Product() {
    std::cout << name_ << " destructor...\n";
}

The syntax is unique as it has no parameters and has the ~ prefix. You can also have only one destructor in a class. What’s more, a destructor doesn’t return any value.

Now, let’s create two objects of that type:

Ex 5.1. A logging destructor, continuation. Run @Compiler Explorer
int main() {
   {
       Product tvset("TV Set", 123);
   }
   {
       Product car("Mustang", 999);
   }
}

In our case, the constructor and the destructor is used to perform the logging. When you run the example, you’ll see the following output:

TV Set, id 123
TV Set destructor...
Mustang, id 999
Mustang destructor...

I specifically enclosed objects (created on the stack) in separate scopes so that their lifetime ends when their scope ends. On the other hand, if we have code:

int main() {
    Product tvset("TV Set", 123);
    Product car("Mustang", 999);
}

Then both tvset and car share the same lifetime scope so that we can expect the following output:

TV Set, id 123
Mustang, id 999
Mustang destructor...
TV Set destructor..

As you can see, the destructors are called in the reverse order of how they were created. It’s because the stack is a LIFO structure (Last In First Out). tvset was created first and added to the stack, then car is added. When the function goes out of the scope, the stack is cleared, taking elements in the reverse order. So car is deleted first, and then tvset. This is illustrated by the following diagram:

Adding and removing objects from the stack.
Adding and removing objects from the stack.

Objects allocated on the heap

Content available in the full version of the book.

Destructors and data members

Content available in the full version of the book.

Virtual destructors and polymorphism

Content available in the full version of the book.

Partially created objects

Content available in the full version of the book.

Use Cases

The primary use case for destructors is when you need to release resources allocated in a constructor. For example, you allocate some memory when the object is created, and then the memory must be released to avoid memory leaks. Similarly, you can open a file or a database connection, and then you must ensure the file or the connection is closed when the object goes out of scope. Fortunately, in Modern C++, there are fewer and fewer places where you need custom destructors. For example, when your data members are standard containers (like std::vector<int>, or std::map<std::string, int>) in your classes, then you can rely on default destructors to do the job. Standard containers like std::vector<int> might allocate memory buffers, but they also manage that buffer and release it properly, so you don’t need to take any action when using them in a class.

A compiler-generated destructor

Content available in the full version of the book.