C++ / MEMORY OWNERSHIP AND SMART POINTERS
RAII: cleanup bound to object lifetime
Wrap a resource in a class that acquires in its constructor and releases in its destructor, so every return, break, or throw cleans up exactly once.
What you will learn
- Write a class that acquires in its constructor and releases in its destructor
- Predict destruction order: reverse of construction, on returns and on exceptions alike
- Explain why a throwing constructor skips ~T yet still destroys its built members
- Delete copy operations so two objects never release the same resource
Understanding RAII: cleanup bound to object lifetime
When an automatic object goes out of scope, the compiler emits a call to its destructor at that exact point: at the closing brace, after a return, after a break, and along the path taken when an exception unwinds the stack. That last case is what makes the guarantee worth building on. C++ has no finally block because the destructor is the finally block, written once on the type instead of repeated at every call site, and objects in a scope are destroyed in reverse order of construction so a resource acquired later is released first.
RAII is the discipline of exploiting that guarantee: acquire the resource in the constructor, release it in the destructor, and release it nowhere else. The payoff is an invariant — if a Handle object exists, its handle is open — which turns "did I remember to release this?" into "is this object still alive?", a question the compiler already answers for you. Pick the storage that matches the lifetime you want: a local variable for a scope, a data member for as long as the owner lives. std::lock_guard and the smart pointers later in this section are this same pattern applied to mutexes and allocations.
Two rules keep the pairing exact. A destructor runs only for an object whose constructor completed, so a constructor that throws skips its own class's destructor while the members it already initialized are still destroyed — which is why each resource belongs in its own RAII member instead of being cleaned up by the enclosing destructor. And because exactly one object must perform the release, a freely copyable handle class is a double-release waiting to happen: delete the copy operations, or give the type move operations that empty the source.
<iostream>
<stdexcept>
int open_handles = 0; // stands in for a real resource table
class Handle {
public:
explicit Handle(int id) : id_(id) { // acquire here, nowhere else
++open_handles;
std::cout << "acquire " << id_ << " (open=" << open_handles << ")\n";
}
~Handle() { // release on every exit path
--open_handles;
std::cout << "release " << id_ << " (open=" << open_handles << ")\n";
}
Handle(const Handle&) = delete; // exactly one object releases
Handle& operator=(const Handle&) = delete;
private:
int id_;
};
void job(bool fail) {
Handle a(1);
Handle b(2);
if (fail) throw std::runtime_error("job failed");
std::cout << "job done\n";
}
int main() {
job(false);
try {
job(true);
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n";
}
std::cout << "still open: " << open_handles << "\n";
}
A destructor is the language's guaranteed cleanup hook, so binding a resource's release to an object's lifetime makes every exit path — return, break, or exception — release it exactly once.
Worked examples
A constructor that throws
Shows that a failed construction skips the class's own destructor but still destroys the members that were already built.
<iostream>
<stdexcept>
struct Part {
explicit Part(const char* n) : n_(n) { std::cout << "Part " << n_ << " built\n"; }
~Part() { std::cout << "Part " << n_ << " destroyed\n"; }
const char* n_;
};
struct Widget {
Part first{"first"};
Part second{"second"};
Widget() {
std::cout << "Widget body running\n";
throw std::runtime_error("ctor failed");
}
~Widget() { std::cout << "~Widget ran\n"; }
};
int main() {
try {
Widget w;
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n";
}
}
Example explained
Line 1Members are initialized before the constructor body runs, so both Parts exist by the time the throw happens.
Line 2"~Widget ran" never prints: a destructor is only called for an object whose constructor completed, and this one did not.
Line 3The two fully built members are destroyed in reverse declaration order while the exception unwinds, so each Part still cleans up.
Line 4Consequence: cleanup you need must live in a member's destructor, because ~Widget is not guaranteed to run at all.
Commit or roll back on every path
Uses a destructor to decide the final action, so a function with several returns can never leave a transaction half-finished.
<iostream>
class Transaction {
public:
Transaction() { std::cout << "BEGIN\n"; }
void commit() { committed_ = true; }
~Transaction() { std::cout << (committed_ ? "COMMIT\n" : "ROLLBACK\n"); }
private:
bool committed_ = false;
};
bool save(int value) {
Transaction t;
if (value < 0) {
std::cout << "rejected " << value << "\n";
return false;
}
std::cout << "wrote " << value << "\n";
t.commit();
return true;
}
int main() {
save(7);
save(-1);
}
Example explained
Line 1Declaring `Transaction t;` opens the transaction; there is no separate begin() call a caller could forget.
Line 2The early `return false` still runs ~Transaction, which sees committed_ == false and rolls back.
Line 3commit() only flips a flag, so the destructor stays the single place that finishes the work and each call ends in exactly one COMMIT or ROLLBACK.
Line 4A throw between BEGIN and commit() would reach the same destructor during unwinding and roll back too.
Important notes
Destructors are implicitly noexcept, and one that throws while an exception is already unwinding calls std::terminate; do best-effort cleanup there and expose an explicit method for errors the caller must see.
A destructor fires when a lifetime ends, so `new Guard(...)` with no matching delete never cleans up — the guard itself must be owned by something whose own lifetime ends.
Common mistakes
Splitting the acquire out into a later open() or init() call: between construction and that call the object exists without the resource, so any early return in the gap leaks and the destructor now needs an "am I actually holding anything?" check.
Releasing by hand and letting the destructor release as well: the second release runs on a stale handle, which is a double free or a close of a descriptor number that has since been reused by something else.
Leaving the wrapper copyable, so `Handle b = a;` duplicates the raw id and both destructors release it at the end of the scope.
Try it yourself
Change, predict, then run
Write a Guard class that prints its name on construction and on destruction, then a function that creates Guard g1("outer"), opens a nested block containing Guard g2("inner"), and throws from inside that block. Catch the exception in main and check that the printed order proves both guards were released before the catch body ran.
Open the C++ workspaceCheck your understanding
A Session class has two RAII data members and its constructor throws after both members have been fully initialized. What cleanup happens?
- ~Session runs first, then both members are destroyed, exactly as in a normal destruction
- Nothing is destroyed; a constructor that throws leaves its members leaked
- Both members are destroyed in reverse declaration order, and ~Session is not called
- Only the second member is destroyed, because the first one was finished earlier and is left alone
Show answer
A destructor is paired with a constructor that completed. Session's never completed, so no Session object exists to destroy and ~Session is skipped; the members that did finish are destroyed in reverse order as the exception propagates. Option 1 is tempting because it looks like ordinary destruction, but running ~Session would hand it a half-built object — which is exactly why the language skips it, and why any cleanup you rely on has to live in a member's destructor.