Skip to content
VLSI Mentor

C++ Part I · Module 12 · Constructors and Destructors

Destructors

Six chapters have been spent on bringing an object into existence, and the module's organising claim was about two events. The other one has been visible in every trace and discussed in none of them: Chapter 12.1's measurement showed a destructor body running while its members were still alive, Chapter 12.2's throwing constructor showed destructors running for the members that had been built, and every program in this module has ended with objects being taken apart in an order nobody wrote down. This chapter is that side. The job is short to state — release whatever the object acquired — and the part that needs measuring is when it happens, because an object's storage duration determines that entirely and the answers are not uniform. A local in a loop body is created and destroyed on every iteration. A static local is created once no matter how many times its function is called. A temporary is destroyed inside the statement that made it, before the next statement begins. A dynamically allocated object is destroyed exactly when something says so, and if nothing does, it is never destroyed at all. One traced program covers all of them. The chapter also settles the question Chapters 12.1 and 12.2 both deferred to it, which is what happens to the members of an object whose constructor throws, because that answer is what makes the next chapter's idiom safe rather than merely tidy.

Intermediate18 min readC++Part IFundamentalsDestructors

Part I · Module 12 · Chapter 12.7 · Destructors

1. The Form and the Job

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    class Handle {
    public:
        explicit Handle(const char* name) { /* acquire */ }
        ~Handle()                         { /* release */ }
    };

A destructor is written ~ClassName, takes no parameters, returns nothing, and cannot be overloaded — a type has exactly one. There is nothing else to the syntax.

Its job:

Release whatever the object acquired.

If the object acquired nothing, it needs no destructor and the compiler's implicit one is correct. That is the common case and Chapter 13.11's Rule of Zero is the destination; this chapter is about what happens when the object did acquire something.

2. What Counts as "Acquired"

Not only memory. Chapter 10.5's callout made this point and it is worth repeating here because it determines whether your type needs a destructor at all:

Memory — new/new[] from Chapter 10.3.

A file handle, a socket, a device handle.

A lock, which must be released on every path or the program stops.

A registration — the object added itself to a list and must remove itself.

A simulator or vendor API handle that must be returned.

For all of these the destructor is the same shape, which is why Chapter 12.9 can treat them uniformly.

3. Two Questions, Two Chapters

There are two separate things to know about destruction and this chapter owns one of them.

When does the destructor run? — §4 to §7, and it depends on storage duration.

In what order are the members and base classes destroyed, once it does? — Chapter 12.8, the next chapter.

Chapter 12.1 §3 already measured the inner shape and it is worth having in mind:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  Owner body of destructor runs (members still alive)
    member c destroyed
    member b destroyed
    member a destroyed

The destructor body runs first, with every member still valid, and the members are destroyed afterwards in reverse declaration order.

4. When It Runs — The Trace

One program, every storage duration.

Azvya Education Pvt. Ltd.VLSI Mentor
dtor.cpp — complete program
#include <cstdio>

struct T {
    const char* n;
    explicit T(const char* x) : n(x) { std::printf("  + %s\n", n); }
    ~T()                             { std::printf("  - %s\n", n); }
};

static T globalObj("static: global");

static void automaticCase()
{
    std::printf("automaticCase entered\n");
    T a("automatic: a");
    for (int i = 0; i < 2; ++i) { T loop("automatic: loop body"); }
    std::printf("automaticCase returning\n");
}

static T& staticLocalCase()
{
    static T s("static local: first call only");
    return s;
}

int main()
{
    std::printf("main entered\n\n");
    automaticCase();

    std::printf("\nstatic local, called twice:\n");
    staticLocalCase(); staticLocalCase();

    std::printf("\ndynamic:\n");
    T* d = new T("dynamic: deleted");
    delete d;
    T* leaked = new T("dynamic: never deleted");
    (void)leaked;

    std::printf("\ntemporary in a full expression:\n");
    std::printf("  about to create a temporary\n");
    (void)T("temporary").n;
    std::printf("  statement finished\n");

    std::printf("\nmain returning\n");
    return 0;
}

Measured output, complete:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  + static: global
main entered

automaticCase entered
  + automatic: a
  + automatic: loop body
  - automatic: loop body
  + automatic: loop body
  - automatic: loop body
automaticCase returning
  - automatic: a

static local, called twice:
  + static local: first call only

dynamic:
  + dynamic: deleted
  - dynamic: deleted
  + dynamic: never deleted

temporary in a full expression:
  about to create a temporary
  + temporary
  - temporary
  statement finished

main returning
  - static local: first call only
  - static: global

5. Reading the Trace

Automatic — at the closing brace of its scope. a was destroyed when automaticCase returned. Guaranteed, on every path, which Chapter 10.8 §8 measured extending to exception propagation.

A loop body is a scope. loop was constructed and destroyed twice — once per iteration. This is the row people forget, and it matters: a resource acquired in a loop body is released each iteration, which is usually what you want and occasionally a performance problem worth knowing about.

Static — after main returns. globalObj was constructed before main entered and destroyed after main returning, which Chapter 10.5 §5 also measured.

A static local — constructed once, on first use. Two calls to staticLocalCase() produced one + line. It is then destroyed at program exit along with the other static objects. Chapter 7.4 owns static objects; this is their destruction timing.

Dynamic — exactly at delete, and never otherwise. dynamic: deleted was destroyed at its delete. dynamic: never deleted produced no - line at all — Chapter 10.5 §4's missing-destructor picture, which is what a leak actually is.

A temporary — at the end of the full expression. + temporary and - temporary both printed before statement finished. The temporary lived for one statement and no longer, which is Chapter 12.10's main subject and the reason Chapter 9.6 §5's dangling string_view was a bug.

And at exit, in reverse order of construction. The static local was destroyed before the global, because the global was constructed first. Chapter 7.4's initialization-order caveats apply to destruction too.

6. The Summary Table

Storage durationDestructor runsGuaranteed?
automaticat the closing brace of its scopeyes, every path including exceptions
automatic in a loop bodyevery iterationyes
static / globalafter main returns, reverse of constructionyes
static localat program exit; constructed once on first useyes
dynamicexactly at deleteonly if something calls it
temporaryat the end of the full expressionyes

The one row without a guarantee is the dynamic one, which is the whole of Module 10's argument and the reason Chapter 12.9 exists: put the delete in a destructor, and it inherits a guaranteed row.

7. The Guarantee Is the Point

Five of the six rows above happen whether or not anyone remembered. That is the property the next two chapters are built on, and it is worth stating as a claim:

For every storage duration except dynamic, the destructor runs on every path out of the scope, including paths created by exceptions.

Chapter 10.8 §8 measured the exception case directly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  + a acquired
  + b acquired
  throwing now
  - b released
  - a released
  caught: boom

Both destructors ran, innermost first, before the catch body. No code anywhere said to do that.

8. When a Constructor Throws

Chapter 12.1's callout and Chapter 12.2 §8 both deferred this here. Measured, two cases.

The body throws, after all members are built:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
constructor body throws, after both members built:
    + member a
    + member b
  body begins; about to throw
    - member b
    - member a
  caught: ctor failed

The members were destroyed in reverse order, and ~Throws — the object's own destructor — never ran.

A member's constructor throws, partway through:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
throw while building the SECOND of three members:
    + member a
    - member a
  caught: member ctor failed

Only a had been built, so only a was destroyed. c was never started, and again the object's own destructor did not run.

The rule, and it is exactly the right one:

If construction does not complete, the object never existed — so its destructor does not run, but everything that was built is destroyed, in reverse order.

That is consistent rather than surprising once you have Chapter 10.5 §1's definition: the lifetime begins when the constructor completes. A constructor that throws never started a lifetime, so there is nothing for a destructor to end.

And it is what makes Chapter 12.9 safe. If a constructor acquires two resources and the second acquisition throws, the first is released by its own member's destructor — automatically, with no try/catch in your constructor. Chapter 12.9's whole idiom depends on this paragraph.

9. When a Destructor Does Not Run

Three cases, in descending order of how often they matter.

A dynamically allocated object that is never deleted. §4 measured it — no - line. This is Chapter 10.6's leak, and it is the case that actually happens.

An object whose construction did not complete — §8. Correct behaviour, not a failure.

And abnormal termination. If the program calls std::abort, or crashes, or std::exit is called while automatic objects are alive, destructors for those automatic objects do not run. Chapter 10.4 §5's mismatched delete ended in SIGTRAP with exactly this consequence. Static objects are destroyed by std::exit but not by abort.

The practical consequence: a destructor is a reliable place to release a resource within a running program, and not a guarantee that survives the process dying. Anything that must be correct even after a crash needs more than a destructor — which is a real constraint for a long-running simulation writing results to disk.

One more thing never to do, named here and belonging to Chapter 22.6: a destructor must not let an exception escape. During exception propagation another exception leaving a destructor terminates the program. Destructors should release and not much else.

10. What This Chapter Cannot Cover

One omission is important enough to name rather than leave to its own module.

If you ever delete an object through a pointer to a base class, the destructor that runs may not be the one you expect, and the derived part of the object may not be destroyed at all. The fix is a single keyword on the base class's destructor, and it is Chapter 17.5's subject — Virtual Destructors.

Nothing in this module involves inheritance, so nothing here is wrong. It is flagged because the failure is silent and the circumstances are common.

11. What to Actually Do

Write a destructor only if the object owns something that must be released. Otherwise write none.

Prefer members that manage themselves — std::string, std::vector, std::unique_ptr — and write nothing.

Expect automatic, static, static-local and temporary destruction to be guaranteed, including on exception paths.

Remember a loop body is a scope — construction and destruction happen each iteration.

Treat the dynamic row as the only unguaranteed one, and put the delete in a destructor (Chapter 12.9).

Know a temporary dies at the end of its full expression — Chapter 9.6 §5's dangling view is the consequence.

Never let an exception escape a destructor (Chapter 22.6).

Do not rely on destructors after abnormal termination — abort runs none.

If you write a destructor, decide immediately what copying means — Chapter 13.4.

And add virtual to a base class's destructor if anything will be deleted through a base pointer (Chapter 17.5).

12. Mistakes Worth Making Once

Writing a destructor for a type that owns nothing. Ceremony, and an empty body costs triviality (Chapter 12.4 §5).

Writing a destructor and leaving copy semantics default. Chapter 10.7's double delete, exit 133.

Forgetting a loop body is a scope. Measured: two constructions, two destructions, for two iterations.

Expecting a static local to be constructed on every call. Measured: one + line for two calls.

Expecting program exit to release dynamic objects. Measured: no - line at all.

Expecting a temporary to survive its statement. Measured: destroyed before the next statement began.

Assuming the object's destructor runs when its constructor threw. Measured: it does not; built members are destroyed instead.

Letting an exception escape a destructor. Terminates during propagation.

Relying on destructors after abort. None run.

Omitting virtual on a base class destructor. Silent, and Chapter 17.5's subject.

13. Interview Relevance

"When does a destructor run?" The answer should be a list by storage duration, not a single sentence: closing brace for automatic, every iteration for a loop body, delete for dynamic, end of the full expression for a temporary, after main for statics. The loop and temporary rows are the ones that distinguish a real answer.

"What happens if a constructor throws?" Members already built are destroyed in reverse order and the object's own destructor does not run, because the lifetime never began. This is commonly unknown and it is the fact that makes RAII safe.

"Can a destructor be overloaded or take parameters?" No — exactly one per type.

"When does a destructor not run?" A leaked dynamic object, a construction that did not complete, and abnormal termination. Being able to give all three is a strong answer.

"Should every class have a destructor?" No — only ones that own a resource. The Rule of Zero is the better default, and mentioning that a destructor implies a decision about copying is the senior answer.

"Why must a destructor not throw?" Because during exception propagation a second exception terminates the program.

14. What You Should Be Able to Say Now

  1. ~ClassName, no parameters, no return, exactly one per type — and its job is to release what was acquired.
  2. "Acquired" includes files, locks, registrations and handles, not only memory.
  3. The destructor body runs first, members still alive; members are destroyed after, in reverse (Chapter 12.1 §3).
  4. Measured — automatic: destroyed at the closing brace.
  5. Measured — a loop body is a scope: constructed and destroyed twice for two iterations.
  6. Measured — static: constructed before main, destroyed after it.
  7. Measured — static local: one construction for two calls; destroyed at exit.
  8. Measured — dynamic: destroyed exactly at delete; the leaked one produced no - line.
  9. Measured — temporary: destroyed within its statement, before the next began.
  10. Measured — at exit: the static local was destroyed before the global — reverse of construction.
  11. Five of six rows are guaranteed on every path, including exception propagation (Chapter 10.8 §8).
  12. Measured — a throwing constructor: built members destroyed in reverse; the object's own destructor never ran.
  13. Because the lifetime begins when the constructor completes — so there was nothing to end.
  14. That is what makes resource acquisition in a constructor safe (Chapter 12.9).
  15. It does not run for a leaked dynamic object, an incomplete construction, or after abort.
  16. Never let an exception escape; add virtual for base-pointer deletion (Chapter 17.5).
  17. Most types need no destructor — the Rule of Zero (Chapter 13.11).

15. Exercises

Exercise 1 — predict the trace. A tracing type is used as: a local, a local inside a three-iteration loop, a new without delete, and a temporary in a printf argument. Write the output.

Exercise 2 — the table. Reproduce §6's six rows from memory, and mark which lacks a guarantee.

Exercise 3 — the static local. Say how many times the constructor runs if its function is called 100 times, and when the destructor runs.

Exercise 4 — throwing construction. For a class with members a, b, c whose second member's constructor throws, list every destructor that runs.

Exercise 5 — does it need one? Decide for each: a RegisterField with a std::string and ints; a type holding new int[n]; a type holding a std::vector; a lock guard; a type holding a FILE*.

Exercise 6 — the three non-runs. Name the three cases where a destructor does not run, and say which is a bug.

Exercise 7 — the follow-up question. A colleague adds a destructor that calls delete data_. State the next question you would ask them and why.

Exercise 8 — the temporary. Explain why const char* p = std::string("hi").c_str(); is a bug, citing the measured temporary row.

16. Summary

  • ~ClassName, one per type; its job is to release what was acquired — files and locks, not only memory.
  • The body runs first, then members in reverse (Chapter 12.1 §3).
  • Measured, one program, every duration: automatic at the brace; a loop body each iteration; static after main; a static local once; dynamic exactly at delete; a temporary within its statement; and at exit, reverse of construction.
  • The leaked dynamic object produced no destructor line at all — Chapter 10.5 §4's picture of a leak.
  • Five of six rows are guaranteed on every path, exception propagation included.
  • Measured: a throwing constructor destroys built members in reverse and the object's own destructor never runs — because the lifetime never began, which is what makes Chapter 12.9 safe.
  • It does not run for a leak, an incomplete construction, or after abort.
  • Never throw from one; virtual for base-pointer deletion (Chapter 17.5).
  • Most types need none — compose from members that manage themselves (Chapter 13.11).

The guarantee is established and one piece of it has been stated twice without being proved. Chapter 12.1 §5 claimed destruction is the exact reverse of construction and this chapter has shown the two ends of that — the destructor body before the members, the members in reverse declaration order — without examining the middle. That matters more than symmetry for its own sake, because it is what lets one member depend on another having been built, and because the order is decided by something other than where you wrote the initializers. The next chapter measures all of it: the order members are built in, what the initializer list does and does not control, the compiler warning that catches the mismatch, and the reason the whole arrangement has to work the way it does rather than the way it looks like it should.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.