Programming glossary 81 terms mentioned in this lesson
Select any term for a clear definition. The return button brings you back to the exact term link you used.
Lesson: Destructors — Correct Cleanup at Lifetime End
To a beginner, a C++ destructor can feel dangerous: it always runs (often invisibly) and one mistake can leak memory or double-delete. Used well, it is the keystone of RAII (Resource Acquisition Is Initialisation) and makes code safe, predictable, and easy to reason about.
Definition: A destructor is a special member function named ~ClassName() that is invoked when an object’s lifetime ends. Its role is to release ownership of resources (heap memory, files, sockets, mutexes, GPU buffers, etc.) while preserving invariants and encapsulation.
Programming Insight (AI) — Do I need a custom destructor?
- Paste your class and ask: “Does this type own resources? Which Rule (3/5/0) applies? Can we remove the destructor via RAII?”
- Request a “minimum ownership surface” rewrite: replace raw handles with
std::unique_ptr/containers and show a diff.
1) A Minimal Example
struct C1 {
int* a{ new int{0} }; // acquires resource
explicit C1(int v): a(new int{v}) {}
~C1() { delete a; } // releases resource (matches new)
bool operator>(int rhs) const { return *a > rhs; }
};
Key points:
- Match allocation/deallocation forms:
new↔delete,new[]↔delete[]. - Prefer not owning raw pointers in user types; use RAII wrappers so destruction is automatic.
2) When Does a Destructor Run?
- Automatic storage: end of scope (including exceptions).
- Dynamic storage: when
delete p;executes on a pointer created withnew. - Members & bases: members are destroyed first (reverse declaration order), then the base class; construction is the opposite.
- Polymorphic delete: if you delete through a base pointer/reference, the base must have a virtual destructor.
struct Base { virtual ~Base() = default; };
struct Derived : Base { ~Derived(){ /* free derived-only resources */ } };
void f(Base* p){ delete p; } // OK: virtual ~Base ensures ~Derived runs
Programming Insight (AI) — Polymorphic Safety Audit
- Ask AI to scan your hierarchy and flag any base used polymorphically that lacks a
virtualdestructor; propose minimal fixes.
3) Rule of 3 / 5 / 0
If a class manages a resource, copying/moving must be defined carefully.
- Rule of 3: define destructor, copy constructor, copy assignment.
- Rule of 5: add move constructor and move assignment for efficiency.
- Rule of 0: prefer RAII so no special members are needed.
// Raw owner (needs Rule of 5)
class Buffer {
char* data{}; std::size_t n{};
public:
explicit Buffer(std::size_t n): data(new char[n]{}), n(n) {}
~Buffer(){ delete[] data; }
Buffer(const Buffer& o): data(new char[o.n]), n(o.n){ std::copy(o.data,o.data+o.n,data); }
Buffer& operator=(const Buffer& o){
if(this!=&o){ char* t=new char[o.n]; std::copy(o.data,o.data+o.n,t); delete[] data; data=t; n=o.n; }
return *this;
}
Buffer(Buffer&& o) noexcept: data(o.data), n(o.n){ o.data=nullptr; o.n=0; }
Buffer& operator=(Buffer&& o) noexcept{
if(this!=&o){ delete[] data; data=o.data; n=o.n; o.data=nullptr; o.n=0; }
return *this;
}
};
// Rule of 0 via RAII
#include <memory>
class Buffer2 {
std::unique_ptr<char[]> data; std::size_t n{};
public:
explicit Buffer2(std::size_t n): data(std::make_unique<char[]>(n)), n(n) {}
// No custom destructor/copy/move needed.
};
Programming Insight (AI) — Rule Selector
- Ask: “Which Rule should this class follow and why?” If Rule-of-0 is possible, request the full rewrite and trade-off notes.
4) Const-Correctness, Exceptions & Determinism
- No throwing from destructors during stack unwinding; catch and log instead.
- Destructors are implicitly
noexcept(true)if they don’t throw. - Deterministic teardown enables timely release of scarce resources (files, locks, GPU memory).
5) Common Pitfalls (and how to avoid them)
- Double delete: two objects own the same raw pointer. Fix: deep copy, disable copy, or use RAII.
- Mismatched forms:
new[]withdelete. Fix: pair correctly. - Missing virtual in base: deleting via base without a virtual destructor is UB. Fix:
virtual ~Base() = default; - Freeing what you don’t own: unclear ownership. Fix: document ownership; avoid ambiguous lifetimes.
- Self-assignment bugs: copy assignment must guard
if (this != &rhs).
Programming Insight (AI) — Invariant Guard
- Ask AI to add asserts that mirror invariants (e.g.,
data!=nullptr || n==0) and generate tests that would fail on regressions.
6) Worked Example — From Raw Handle to RAII
// Before: brittle raw handle
typedef FILE* FileHandle;
class File {
FileHandle f{};
public:
explicit File(const char* path){ f = std::fopen(path, "rb"); }
~File(){ if(f) std::fclose(f); }
File(const File&) = delete; // unique ownership
File& operator=(const File&) = delete;
};
// After: RAII with custom deleter
#include <memory>
struct FCloser { void operator()(FILE* p) const noexcept { if(p) std::fclose(p); } };
class File2 {
std::unique_ptr<FILE, FCloser> f_{};
public:
explicit File2(const char* path): f_( std::fopen(path, "rb") ) {}
// Rule-of-0: destructor, move, copy semantics handled by unique_ptr
};
Programming Insight (AI) — Refactor with Rationale
- Have AI provide: (1) the RAII rewrite, (2) the copy/move implications in two bullets, (3) unit tests for double-delete and use-after-free.
7) Mini Exercises
- Polymorphic base: Write a base/derived pair; show correct vs incorrect deletion via base pointer and explain the UB in the latter.
- Mismatched forms: Implement a small class using
new[]; fix the bug wheredeletewas used instead ofdelete[]. - Rule-of-0 refactor: Replace a raw
char*owner withstd::unique_ptr<char[]>; list which special members you were able to delete.
Programming Insight (AI) — Check My Destructor
- Paste your solutions; AI will flag missing moves, missing
virtualon bases, and mismatcheddelete/delete[].
Summary Checklist
- Prefer RAII & Rule-of-0: eliminate manual
deletewhere possible. - Virtual destructor in polymorphic bases if deleting via base pointer/reference.
- Match allocation forms:
new/delete,new[]/delete[]. - Don’t throw in destructors during stack unwinding.
- AI accelerates the grunt work; you remain the architect ensuring ownership semantics are correct.
Advanced perspective: destruction is the final ownership decision
A destructor is correct when lifetime ends without leaving ownership unfinished
The C++ term is destructor, not deconstructor. A destructor is a special member function that participates in ending an object's lifetime and performing the teardown required by its type. It does not reverse the constructor statement by statement. It also does not mean that every class should contain a hand-written cleanup body.
The easy proxy is to look for delete, close or release inside ~ClassName(). That proves only that a function has been called. The real criterion is ownership: every acquired resource has one responsible owner, copying and moving preserve that responsibility, partial construction remains safe, and every ordinary lifetime path reaches the appropriate release exactly once.
Start with an observable lifetime, not a raw allocation
#include <iostream>
#include <string>
#include <utility>
class Trace {
public:
explicit Trace(std::string name) : name_{std::move(name)} {
std::cout << "construct " << name_ << '\n';
}
~Trace() {
std::cout << "destroy " << name_ << '\n';
}
private:
std::string name_;
};
int main() {
Trace first{"first"};
Trace second{"second"};
}
The program produces four lines:
construct first
construct second
destroy second
destroy first
first completes construction before second. When the block exits, the constructed automatic objects are destroyed in reverse order. The output is evidence for this execution, not a decorative slogan: each construction has one corresponding destruction, and the later object disappears before the earlier object on which it might depend.
| Event | Live objects after the event | Reason |
|---|---|---|
Construct first. | first | Its declaration has completed. |
Construct second. | first, then second | The next declaration has completed. |
Destroy second. | first | The block is leaving in reverse construction order. |
Destroy first. | None of these objects. | The remaining local lifetime ends. |
A destructor body is only the first visible step of structural teardown
For a complete derived object, the most-derived destructor body runs while its members and base subobjects are still available. Then non-static data members are destroyed in reverse declaration order, followed by direct base subobjects in reverse construction order; virtual bases are destroyed as part of the complete object's final base teardown. Array elements are likewise destroyed in reverse construction order.
| Part of a complete object | Destruction position | Design consequence |
|---|---|---|
| Most-derived destructor body | First. | It may coordinate teardown while members still exist. |
| Non-static data members | Reverse declaration order. | Declare dependencies so later members can disappear first. |
| Direct non-virtual bases | After members, in reverse base construction order. | Derived cleanup must not expect a base already to have vanished. |
| Virtual bases | Last for the complete object. | The most-derived object owns their single construction and final teardown. |
The member-initialiser list cannot change member declaration order. If a member named writer uses a member named file during destruction, their declaration order must make that dependency safe. A clever-looking initialiser order does not renegotiate the class layout or its destruction sequence.
Object lifetime and storage are connected, but they are not the same thing
An automatic object's destructor runs when its block exits normally or through stack unwinding. Its storage belongs to the automatic-storage mechanism. For the usual dynamic-allocation path, a delete-expression requests both destruction of the object and release of the allocation; those remain separate responsibilities even though one expression initiates both. A destructor can release a file or mutex without deallocating the storage occupied by the owning object.
This distinction removes two common errors. First, delete operates on an appropriate pointer expression, not a reference. Secondly, calling a destructor is not a general replacement for delete, because an explicit destructor call does not by itself establish the matching storage-deallocation policy. Most application code should express ownership through established RAII types rather than manage either step explicitly.
Partial construction is already an ownership test
If a constructor exits by throwing, the complete object's destructor is not called because that complete object was never successfully constructed. Subobjects whose construction completed are destroyed in reverse completion order. A raw resource acquired between subobject construction and the throw can therefore leak unless it has already been placed under an owner.
This is one of RAII's strongest consequences. Store a file, allocation, lock or handle in a member whose own construction establishes ownership. If a later member fails, the earlier owner is destroyed automatically during unwinding. The containing class does not need to guess how far its constructor progressed, and a destructor for an object that never existed is not asked to repair the situation.
Rule of Zero is the default; the other rules are ownership reviews
If members and bases already express the correct resource semantics, allow their special members to compose. That is the Rule of Zero. It does not promise that every operation exists. A std::unique_ptr member normally makes copying unavailable while allowing exclusive ownership to move. A std::string or std::vector normally supports value copying. The generated interface follows the members' capabilities.
| Representation | Likely ownership meaning | Special-member review |
|---|---|---|
Value members such as std::string | The object owns its value. | Generated copy, move and destruction are often correct. |
std::unique_ptr<T> | Exclusive transferable ownership. | Copy is unavailable; move and destruction follow the member. |
std::shared_ptr<T> | Shared lifetime responsibility. | Copy shares ownership; the last owner releases the object. |
| Raw non-owning pointer | Observation without ownership. | The destructor must not release the pointee; lifetime validity is external. |
| Raw owning handle | Custom responsibility not yet encoded. | Prefer an existing owner or design copy, move, assignment and destruction together. |
If a type directly manages a raw resource, a custom destructor raises the Rule-of-Three questions about copy construction and copy assignment. Move construction and move assignment add the Rule-of-Five questions. A user-declared destructor also prevents the usual implicit declaration of a move constructor, so adding an apparently harmless destructor can make an rvalue copy instead or make the operation unavailable. The rule names are prompts to inspect the complete ownership interface, not quotas for boilerplate.
Polymorphic destruction is a base-interface policy
If clients may destroy a derived object through a pointer to its base, the base needs a public virtual destructor. This applies equally when an owning smart pointer stores a Base* and its ordinary deleter eventually performs that deletion. Virtual dispatch then reaches the derived destructor before the base subobject is destroyed.
There is a second coherent policy: make the base destructor protected and non-virtual when destruction through a base pointer is deliberately forbidden. The access control prevents clients from making the invalid request. A public non-virtual destructor on a polymorphic base offers an operation that cannot safely complete the derived lifetime. Merely adding another virtual member function does not fix that contradiction.
Cleanup failure needs a policy before the destructor begins
A destructor with no explicit exception specification derives that specification from the destructors of its potentially constructed subobjects and, where relevant, virtual bases. It is therefore inaccurate to say that a destructor is non-throwing simply because its body contains no written throw. The members participate in the rule.
Nevertheless, ownership destructors should be designed not to let exceptions escape. If a non-throwing destructor exits through an exception, the program terminates. If a destructor directly invoked during stack unwinding throws, termination also follows because another exception is already being handled. Catching and ignoring every failure is not automatically correct either.
If a release operation can fail and the caller must react, expose an explicit operation such as close() that reports the result while normal error handling remains possible. The destructor can provide a non-throwing fallback and preserve process-local safety, but it cannot promise that an external transaction was committed merely because the C++ object went out of scope.
Deterministic destruction is an ordinary-path guarantee, not an immortality policy
| Exit path | What happens to relevant automatic objects? | What this proves |
|---|---|---|
| Normal block exit or early return | Constructed local objects are destroyed. | RAII handles ordinary control flow. |
| Exception with stack unwinding | Completed local objects are destroyed on the unwound path. | RAII handles exception paths that actually unwind. |
Return from main | Its local objects are destroyed before normal program termination continues. | Returning is not the same as abandoning the current block. |
std::exit inside a block | Automatic objects in active scopes are not destroyed by that call. | Process termination is not ordinary scope exit. |
std::abort or std::_Exit | Ordinary destructor processing is not performed. | No destructor can guarantee recovery from abrupt termination. |
A destructor is therefore the right place for process-local resource release, not the sole durability mechanism for persistent data. Files may still require flush, sync, commit, journalling or an application protocol. Power loss does not consult the class definition.
Corrections to the earlier destruction examples: deletion operates through a pointer expression, not a reference. The raw C1 owner has a destructor but still permits default copying of its pointer, so two copied objects can attempt to delete one allocation. The Buffer2 and File2 wrappers gain destruction and movable ownership from unique_ptr, but copying is ordinarily deleted rather than automatically implemented. The file example also needs <cstdio> for FILE, std::fopen and std::fclose, plus an explicit opening-failure policy. Finally, a destructor's exception specification depends on its declaration and the destructors of its subobjects; the shorthand "implicitly noexcept(true) if it does not throw" omits the governing language rule.
Programming Insight (AI): audit every route by which ownership can end
For an AI-generated owner, require an ownership table before accepting code. It must identify each resource, the member that owns it, the empty state, acquisition point, release operation, copy meaning, move meaning and failure policy. Then require traces for successful construction, failure after each acquisition, normal scope exit, early return, exception unwinding and a move followed by destruction of both source and destination.
Compile the design and use appropriate runtime diagnostics, but keep the semantic review. A sanitizer can expose a double deletion in an executed path; it does not prove that the proposed copy policy is the policy the domain wanted. A destructor that appears once in generated code is not yet an ownership proof.
Transfer task: design the end of a resource-owning session
Design a Session that owns a log file, a network connection and a temporary receive buffer. Construction may fail after any acquisition. Closing the network connection may report an error that the caller needs to see, while releasing the buffer cannot fail. Decide which existing RAII members should represent the resources and in which declaration order they belong. State whether Session is copyable, movable or neither.
Trace successful construction, failure after each completed member, an early return, an exception during use, an explicit successful close(), a failed close(), and destruction of both sides after a move. If a base interface owns sessions polymorphically, choose and justify either public virtual destruction or protected non-virtual destruction. The result is complete only when every trace releases each acquired resource once and every reportable failure still has a route to the caller.
Reveal answer
The members should own the resources directly. A checked std::ofstream owns the log file, a move-only Connection wrapper owns the native network handle, and std::vector<std::byte> owns the receive storage. The connection wrapper provides both an explicit reporting close() and a non-throwing destructor fallback.
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <string_view>
#include <system_error>
#include <vector>
class Connection {
public:
static Connection connect(std::string_view endpoint); // throws on failure
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
Connection(Connection&&) noexcept;
Connection& operator=(Connection&&) = delete;
~Connection() noexcept; // best-effort release if still owning
[[nodiscard]] std::error_code close() noexcept;
[[nodiscard]] bool is_open() const noexcept;
};
class Session {
public:
Session(const std::filesystem::path& log_path,
std::string_view endpoint,
std::size_t buffer_size);
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
Session(Session&&) = default;
Session& operator=(Session&&) = delete;
~Session() = default;
[[nodiscard]] std::error_code close() noexcept {
return connection_.close();
}
private:
static std::ofstream open_log(const std::filesystem::path& path);
std::ofstream log_; // constructed first, destroyed last
Connection connection_; // constructed second
std::vector<std::byte> buffer_; // constructed last, destroyed first
};
open_log opens and validates the stream before returning it; a stream with only failbit set is not a successful acquisition. The constructor initialises log_ with that helper, then calls Connection::connect, then allocates the buffer. Members are constructed in declaration order, whatever order is written in the member-initialiser list.
This order assumes connection shutdown does not need the receive buffer. It keeps the log alive while the connection is released, so a close outcome can still be recorded. If the connection's release operation genuinely needs the buffer, the buffer must instead be declared before the connection so that it is destroyed afterwards. Dependency decides the order; visual neatness does not.
Session is move-constructible but neither copyable nor move-assignable. Move construction transfers each owner and leaves the source members in their documented empty, destructible states. That requires the Connection move constructor to invalidate the source handle. Move assignment is deleted because replacing an already live destination could otherwise release its connection through an operation which has no route to return the reportable close error. Merely copying the native handle would create two alleged owners and is also forbidden.
| Route | Ownership result | Failure report |
|---|---|---|
| Log acquisition fails | No later member is constructed; the failed stream object cleans up its own partial state. | The constructor reports the opening failure. |
| Connection acquisition fails | The completed log_ member is destroyed during unwinding. | connect reports the connection failure. |
| Buffer allocation fails | connection_ is destroyed first, then log_. | The allocation exception continues after completed members are released. |
| Constructor body fails after buffer acquisition | buffer_, connection_ and log_ are all destroyed in reverse declaration order. | The constructor-body exception continues after all completed members are released. |
| Successful construction | One member owns each resource. | No failure. |
| Early return or exception during use | buffer_, connection_ and log_ are destroyed in reverse declaration order. | A destructor cannot return a close error; code which must react must call close() before leaving. |
Explicit successful close() | The connection relinquishes its handle and becomes empty. Later destruction is a no-op for that handle; the buffer and log remain owned until their normal destruction. | A success error code is returned to the caller. |
Explicit failed close() | Under this wrapper's contract, the close attempt consumes the owned handle and leaves the wrapper empty, even when it reports failure. Later destruction cannot release it again. | The non-zero error code reaches the caller while ordinary error handling is still possible. |
| Move then destroy both objects | The destination releases the transferred resources once. Destruction of the empty source releases none. | Any required reporting close is performed on the destination before destruction. |
The consuming rule for a failed close must be implemented against a network API whose ownership semantics support it. A different platform wrapper may retain ownership for a permitted retry, but that is a different contract and needs a corresponding trace. What Session must not do is guess, retry an already consumed native handle, or discard an error the caller was required to see.
If sessions are owned through a base pointer, the base needs public virtual destruction:
class ISession {
public:
virtual ~ISession() = default;
[[nodiscard]] virtual std::error_code close() noexcept = 0;
};
A protected non-virtual destructor would deliberately prevent deletion through the base and is therefore the wrong policy for the stated owning interface. Public virtual destruction lets std::unique_ptr<ISession> reach the complete derived object. It does not replace explicit close(): polymorphic destruction releases resources, while the reporting operation gives a recoverable network failure to the caller.
Destruction completes the course's treatment of state, scope, ownership and behaviour. The final function in an object's life should contain no surprises because the ownership policy was decided when the type was designed. When that policy is expressed by members, the best custom destructor is often the one you never needed to write.