Resource Cleanup with Destructors
Clean up resources automatically when objects go out of scope.
What Are Destructors?
A destructor is a special member function that runs automatically at the instant an object of a non-aggregate class type is destroyed. A constructor is the first code that runs on a new object; the destructor is the last code that runs while that object still exists. Whatever tidying up the class needs, releasing a resource it holds, or writing a final record somewhere, belongs there.
You never call a destructor yourself, so the useful way to learn one is backwards: first watch exactly when objects die, then decide what is worth attaching to that moment.
Watching objects die
The class below prints on the way in and on the way out, which turns object lifetime into something you can read off the console:
#include <iostream>
#include <string_view>
class Marker
{
private:
std::string_view m_label{};
public:
Marker(std::string_view label)
: m_label{ label }
{
std::cout << "opened " << m_label << '\n';
}
~Marker()
{
std::cout << "closed " << m_label << '\n';
}
};
Marker g_session{ "session" };
int main()
{
Marker outer{ "outer" };
{
Marker firstInner{ "first inner" };
Marker secondInner{ "second inner" };
}
std::cout << "block finished\n";
return 0;
}
Output:
opened session
opened outer
opened first inner
opened second inner
closed second inner
closed first inner
block finished
closed outer
closed session
Three separate rules are visible in those nine lines.
Local objects are destroyed at the closing brace of the block that owns them. firstInner and secondInner are already gone before block finished prints, because the inner braces ended their block. outer survives until main() returns.
Within a block, objects are destroyed in the reverse of the order they were created. secondInner was built last, so it is torn down first. The same reversal applies to any group of objects created together, which matters as soon as a class holds several of them.
Objects with static duration, including globals like g_session and static local variables, are built before main() starts and destroyed after it returns. That is why closed session is the final line.
An object created dynamically is a fourth case, destroyed at the point your code releases it rather than at any brace. We come back to that when we cover dynamic memory.
The rules for writing one
A destructor's declaration is fixed by the language, and there is very little of it:
- It is named with a tilde followed by the class name, so class
Markergets~Marker(). - It takes no parameters.
- It has no return type, not even
void. - A class has exactly one. With no parameters there is nothing to overload on, so a second destructor is impossible.
Two habits follow from that. Do not call a destructor explicitly. It is already scheduled to run when the object is destroyed, and running the cleanup twice is a bug in almost every class that has cleanup worth doing. And do feel free to call other member functions from inside the destructor body: the object is fully intact for the whole time that body is executing, and is only dismantled afterwards.
Cleanup that cannot be forgotten
Here is the problem destructors were invented for. Ledger collects entries in memory and writes them out in one go, because writing each one separately would be wasteful. Committing is a separate member function that the user of the class is expected to call:
#include <iostream>
#include <string>
#include <string_view>
class Ledger
{
private:
std::string m_pending{};
public:
void record(std::string_view entry)
{
if (!m_pending.empty())
m_pending += ", ";
m_pending += entry;
}
void commit()
{
if (m_pending.empty())
return;
std::cout << "committed: " << m_pending << '\n';
m_pending.clear();
}
};
bool settleAccount(bool amountIsValid)
{
Ledger ledger{};
ledger.record("open");
ledger.record("charge");
if (!amountIsValid)
return false;
ledger.commit();
return true;
}
int main()
{
settleAccount(true);
settleAccount(false);
return 0;
}
Output:
committed: open, charge
Two accounts were settled and only one produced any output. The second call took the early return, so commit() was never reached and both of its entries vanished when ledger was destroyed. Notice that nobody forgot to write the call. It is right there in the function. The program simply does not path through it on every route, which is a far easier mistake to make than omitting the call entirely, and a far harder one to spot in review.
Any class holding a resource has this shape. Memory, an open file, a database connection, a network socket, and a lock all need releasing before the object holding them disappears, and record keeping such as flushing a log or sending a final measurement has the same requirement. Relying on the caller to trigger it is relying on every future caller getting every future control path right.
The fix is to stop asking. If the object is being destroyed, the cleanup is due, so attach it to destruction:
#include <iostream>
#include <string>
#include <string_view>
class Ledger
{
private:
std::string m_pending{};
public:
~Ledger()
{
commit();
}
void record(std::string_view entry)
{
if (!m_pending.empty())
m_pending += ", ";
m_pending += entry;
}
void commit()
{
if (m_pending.empty())
return;
std::cout << "committed: " << m_pending << '\n';
m_pending.clear();
}
};
bool settleAccount(bool amountIsValid)
{
Ledger ledger{};
ledger.record("open");
ledger.record("charge");
if (!amountIsValid)
return false;
ledger.commit();
return true;
}
int main()
{
settleAccount(true);
settleAccount(false);
return 0;
}
Output:
committed: open, charge
committed: open, charge
The early return now commits too, and nothing at the call site had to change. The guard in commit() earns its place here: on the successful path the explicit call empties the buffer, so the destructor's call finds nothing left to write and does not duplicate the output.
If a class must do something before it dies, put that something in the destructor rather than in a function the caller is expected to remember. Cleanup that depends on the caller's discipline will eventually be skipped.
The body runs first, then the members
A destructor body is not the whole of destruction. Once the body finishes, the object's data members are destroyed in turn, in reverse order of declaration:
#include <iostream>
class Panel
{
public:
~Panel()
{
std::cout << "panel torn down\n";
}
};
class Toolbar
{
private:
Panel m_panel{};
public:
~Toolbar()
{
std::cout << "toolbar destructor body running\n";
}
};
int main()
{
Toolbar toolbar{};
return 0;
}
Output:
toolbar destructor body running
panel torn down
This ordering is what makes the destructor body safe to write. Every member is still fully constructed while your code runs, so you can read them, print them, and pass them to other functions without worrying that something has already been dismantled underneath you.
When not to write one
If a class has no user-declared destructor, the compiler generates one with an empty body. This is the implicit destructor, and it is a placeholder rather than a service: it does nothing itself, though the members are still destroyed after it, exactly as above.
Most classes want precisely that. A class whose members are plain values or standard library types that clean up after themselves has nothing left to do at destruction, and writing an empty ~ClassName() {} adds a line of code and no behaviour. Write a destructor when there is genuine cleanup to perform, and otherwise let the compiler supply the implicit one.
Where the guarantee stops
Automatic destruction is reliable, but it is not unconditional. std::exit() ends the program on the spot, without unwinding the blocks that are currently executing, so the local objects alive at that moment are simply abandoned and their destructors never run:
#include <cstdlib>
#include <iostream>
#include <string_view>
class Marker
{
private:
std::string_view m_label{};
public:
Marker(std::string_view label)
: m_label{ label }
{
std::cout << "opened " << m_label << '\n';
}
~Marker()
{
std::cout << "closed " << m_label << '\n';
}
};
int main()
{
Marker outer{ "outer" };
std::cout << "leaving now\n";
std::exit(0);
}
Output:
opened outer
leaving now
No closed outer. Had outer been a Ledger, those entries would have been lost with it.
Calling
std::exit() while objects that rely on their destructors are alive silently skips their cleanup. Return normally out of main() instead whenever you can.
An unhandled exception terminates the program too, and is not required to unwind the stack before doing so. Where no unwinding happens, destructors are not run either.
std::abort() behaves the same way. We look at exceptions in detail in a later chapter.
Summary
Every class that is not a plain aggregate gets exactly one destructor, and the compiler fires it as that object goes out of existence. It is named ~ClassName, takes no parameters, and has no return type. You do not call it yourself, and it may call the class's other member functions freely, because the object is still whole until the body finishes.
Destruction happens at predictable moments. Local objects die at the end of their block, in reverse order of creation. Static and global objects die at program shutdown. Once a destructor body returns, the object's members are destroyed in reverse order of declaration.
The reason to write one is cleanup that must not be skipped: releasing memory, files, connections, or locks, and any record keeping the class owes the outside world. Moving that work out of a member function the caller must remember and into the destructor removes an entire class of bug, including the early return that quietly bypasses the call. If a class has no such work, define no destructor and let the compiler generate the implicit one.
The guarantee has limits. std::exit() abandons live local objects without destroying them, and an unhandled exception may terminate without unwinding the stack, so cleanup you cannot afford to lose should not depend on either path.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Resource Cleanup with Destructors - Quiz
Test your understanding of the lesson.
Practice Exercises
Introduction to Destructors
Practice implementing destructors to perform cleanup when objects are destroyed. Learn about automatic destruction and resource management.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!