C++ / CLASSES AND OBJECT LIFECYCLE
Destructors and the order of teardown
Predict and control when and in what order destructors run for locals, members, bases, temporaries and statics, and know when you need to write one yourself.
What you will learn
- Predict a class's teardown trace: body first, then members in reverse declaration order
- Reorder member declarations when one member must outlive another during teardown
- Move manual cleanup into a destructor so early returns and throws cannot skip it
- Explain why an explicit destructor call leads to a second, undefined destruction
Understanding Destructors and the order of teardown
A destructor is the one member function you almost never call yourself: you write ~Session() and the compiler decides where the call goes. For an automatic object that place is the closing brace of its enclosing block, for a temporary it is the semicolon that ends the full expression, for a new-ed object it is the matching delete, and for a static or global it is after main returns. The position is fixed by the object's storage duration rather than by anything you write, which is exactly what makes a destructor a trustworthy place to release something.
Teardown is the mirror image of construction. Within a single object the destructor body runs first, then the non-static members are destroyed in the exact reverse of their declaration order, then base subobjects in reverse order. The reason is dependency direction: a member constructed later may have been handed a reference to an earlier one, and the body may still read every member, so reversing the order guarantees that nothing is destroyed while something that might use it is still alive. The same rule scales up to a block, where locals are destroyed last-built-first, like popping a stack.
The compiler-generated destructor already destroys every member, so you only write your own when the class owns something the type system does not track: a raw new-ed pointer, a FILE*, a lock, an OS handle. The payoff is that the release happens on every exit path, including exception unwinding, because unwinding destroys every fully constructed object it passes on the way out. That is also why a destructor must not let an exception escape: during unwinding it would be a second exception in flight and the program calls std::terminate.
<iostream>
<string>
<utility>
struct Tracer {
std::string name;
explicit Tracer(std::string n) : name(std::move(n)) {
std::cout << " +" << name << '\n';
}
~Tracer() { std::cout << " -" << name << '\n'; }
};
class Session {
public:
Session() : socket("socket"), buffer("buffer") {
std::cout << "Session body\n";
}
~Session() { std::cout << "~Session body\n"; }
private:
Tracer socket; // declared first, so destroyed last
Tracer buffer; // declared second, so destroyed first
};
int main() {
std::cout << "enter main\n";
{
Tracer outer("outer");
Session s;
Tracer inner("inner");
std::cout << "-- end of block --\n";
}
std::cout << "leave main\n";
}
Destruction mirrors construction exactly — destructor body, then members in reverse declaration order, then bases in reverse — at a point fixed by the object's storage duration.
Worked examples
Destructors run during exception unwinding
Shows that scope-based cleanup happens on the normal path and on the throwing path, before any catch block runs.
<iostream>
<stdexcept>
struct FileHandle {
int id;
explicit FileHandle(int i) : id(i) { std::cout << "open " << id << '\n'; }
~FileHandle() { std::cout << "close " << id << '\n'; }
};
void work(bool fail) {
FileHandle a(1);
FileHandle b(2);
if (fail) throw std::runtime_error("boom");
std::cout << "work finished\n";
}
int main() {
work(false);
try {
work(true);
} catch (const std::exception& e) {
std::cout << "caught " << e.what() << '\n';
}
}
Example explained
Line 1a and b are constructed in that order, so the closing pair always prints close 2 then close 1.
Line 2The throw leaves work before any cleanup code could be reached, yet both handles are released, because the compiler emits those destructor calls on the unwinding path too.
Line 3close 2 and close 1 appear before caught boom: unwinding of the throwing scope completes before the handler body starts.
Line 4Note there is no cleanup statement anywhere in work — the ordering guarantee is doing all the work.
Temporaries, statics, and objects that outlive main
Contrasts three different storage durations to show that the destruction point comes from the object's kind, not from where you wrote it.
<iostream>
struct Noisy {
const char* tag;
explicit Noisy(const char* t) : tag(t) { std::cout << "ctor " << tag << '\n'; }
~Noisy() { std::cout << "dtor " << tag << '\n'; }
};
Noisy global("global");
void f() {
static Noisy local_static("static");
std::cout << "f called\n";
}
int main() {
std::cout << "main starts\n";
Noisy("temp");
std::cout << "after temporary\n";
f();
f();
std::cout << "main returns\n";
}
Example explained
Line 1global is constructed before main is entered, which is why ctor global is the very first line of output.
Line 2Noisy("temp"); creates an unnamed object, so it is destroyed at the semicolon ending that statement — dtor temp prints before after temporary.
Line 3local_static is constructed on the first call to f only, so f called appears twice but ctor static once.
Line 4Objects with static storage duration are destroyed after main returns in reverse order of construction, so the static declared inside f dies before global.
Teardown order across a base and derived class
Traces the full reverse chain for a derived object destroyed through a base pointer.
<iostream>
struct Part {
const char* n;
explicit Part(const char* n) : n(n) { std::cout << "ctor " << n << '\n'; }
~Part() { std::cout << "dtor " << n << '\n'; }
};
struct Base {
Part b1{"Base::b1"};
virtual ~Base() { std::cout << "~Base\n"; }
};
struct Derived : Base {
Part d1{"Derived::d1"};
Part d2{"Derived::d2"};
~Derived() override { std::cout << "~Derived\n"; }
};
int main() {
Base* p = new Derived();
delete p;
}
Example explained
Line 1Construction builds the Base subobject first, so ctor Base::b1 precedes the two Derived members.
Line 2delete p reaches ~Derived at all only because ~Base is declared virtual; without it only the Base part would be destroyed and d1 and d2 would leak.
Line 3The ~Derived body prints before d2 and d1 are destroyed, so the body can still touch its own members.
Line 4Base's member b1 is destroyed last of all — it was the first thing built, so it is the last thing released.
Important notes
An early return or a throw inside the destructor body does not skip member and base destruction; those calls are appended after the body by the compiler and always run.
Because static-storage objects are destroyed after main in reverse construction order, a destructor that touches another global may find that object already destroyed.
Common mistakes
Calling obj.~Widget() by hand to "free it early": the automatic destructor call still happens at scope exit, so the object is destroyed twice, which typically means a double free and undefined behaviour.
Assuming members are destroyed in the reverse of the member initialiser list order; the list cannot reorder anything, so shuffling it changes nothing and code written around the assumed order breaks quietly.
Letting an exception propagate out of a destructor: if it fires while the stack is already unwinding, std::terminate is called and the original error is never reported.
Try it yourself
Change, predict, then run
In a browser editor, write a tracing type that prints in both its constructor and destructor, give a class three such members plus a destructor body that prints, and write down the full expected trace before running it. Then move the first member declaration to the bottom of the class, predict the new trace, and confirm that the member initialiser list order had no effect on it.
Open the C++ workspaceCheck your understanding
A class declares Tracer first; then Tracer second;, its constructor's member initialiser list is written : second(...), first(...), and its destructor body prints "body". What does one object of this class print as it goes out of scope?
- body, then second's destructor, then first's destructor
- second's destructor, then first's destructor, then body
- body, then first's destructor, then second's destructor
- first's destructor, then second's destructor, then body
Show answer
The destructor body runs before any member is destroyed, so the body's output comes first; members are then destroyed in the reverse of their declaration order, and second is declared last, so it goes first. Option 3 is what you would expect if the member initialiser list dictated the order, but that list cannot reorder anything — members are initialised in declaration order however it is written, and destroyed in the reverse of that.