cswork / libraryOpen practice workspace

Articles

C++ Exception Safety Interview Questions: Guarantees, RAII, and Failed Construction

Practice C++ exception safety questions on basic and strong guarantees, RAII cleanup, failed constructors, vector growth, and worked failure traces.

“Exception safe” is too vague to evaluate. A C++ interview answer must name the promised guarantee, the program state visible if an operation throws, and the owner of every resource during unwinding. A container operation may leave the old value unchanged, merely preserve invariants, or offer no useful guarantee. The difference becomes visible when a constructor, allocation, or move throws halfway through a multi-step operation.

Official language and guideline facts: the C++ draft's stack-unwinding rules describe destruction of completed subobjects after failed construction; the C++ Core Guidelines recommend RAII and non-failing cleanup; the draft vector modifiers wording places conditions on the no-effects guarantee. Inference: interview answers should prove a guarantee for the shown operation and element type rather than attach one label to all C++ code. The following cases are original exercises, not employer-specific candidate reports.

Question 1: What are the basic, strong, and no-throw guarantees?

The basic guarantee means that if an operation throws, the program remains in a valid state and does not leak owned resources, though observable values may have changed. The strong guarantee means failure has no observable effect on the operation's promised state: clients can continue as if it had not committed. The no-throw guarantee means the operation cannot fail by throwing under its stated conditions. These labels do not replace a contract. Say what state is protected: the object itself, a database, a file, or a remote side effect.

For example, a function that appends to a vector and then writes to a log may provide a strong guarantee for the vector if append fails, but not for the log if the logging step fails after the append. A no-throw swap of two pointer-and-size handles can serve as a commit step, but it does not roll back an external payment or file write. In an interview, identify each side effect before claiming the guarantee.

A concise decision table helps:

GuaranteeOn exceptionWhat you must prove
BasicInvariants and ownership survive; values may changeEvery path releases resources and leaves valid state
StrongPromised state is unchangedWork stays in temporary state until a non-throwing commit
No-throwNo exception escapesAll operations on that path cannot throw, or failure is handled without throwing

An interviewer may ask whether “no leaks” is the same as strong exception safety. It is not. RAII can prevent leaks while a counter, buffer, or external record has already changed. Conversely, a function that restores a value but leaks a file descriptor has not met a useful strong contract because the program's resource state changed. State the scope explicitly.

Question 2: Why does RAII matter during stack unwinding?

RAII binds resource ownership to object lifetime. A unique_ptr owns dynamic memory, a lock_guard owns a lock, and a file-handle wrapper can own a descriptor. When control leaves a scope through a return or exception, destructors release those resources. This is the systemic alternative to manually placing delete or unlock on every branch. The Core Guidelines' RAII rule E.6 makes the leak-prevention rationale explicit.

Consider a function that locks a mutex, builds a string, and updates a cache. String allocation may throw. With a raw lock()/unlock() pair, the exception can skip unlock and leave other threads blocked. A scoped lock_guard releases the mutex during unwinding. This yields resource safety; it does not undo a cache mutation performed before the throw. The basic versus strong distinction still has to be proved.

RAII also helps with early returns and nested failures. A resource-owning class should define what it owns, how ownership transfers, and what its destructor does. Avoid a class with an owning raw pointer and default copy operations, because two copies may both delete the same pointer. The resource-owning C++ class practice question is a useful way to rehearse ownership semantics before discussing exceptions.

Question 3: What is destroyed when a constructor throws?

If construction fails, the complete object was never created, so its ordinary destructor is not called for that failed complete object. The language nevertheless destroys fully constructed base and member subobjects as specified, in reverse completion order; automatic local objects in scope also unwind. The draft rule is more precise than the shorthand “C++ cleans everything up.” Any raw resource acquired in the constructor body and not yet wrapped in a completed owning subobject may leak.

Imagine a class with a unique_ptr member, a vector member, and a constructor body that opens a raw OS handle then throws. The unique_ptr and vector are completed members and clean themselves up. The raw handle acquired later does not magically have an owner; the failed object's destructor will not run to close it. Wrap the handle in a RAII member or local guard immediately after acquisition, then transfer ownership only when the constructor can complete. This is a concrete reason to avoid manual cleanup hidden solely in the destructor.

The construction order matters. Members initialize in declaration order, not the visual order of the initializer list. If member two throws, member one is destroyed; member three was never initialized. An interview answer should write that sequence on the board. A test can inject an exception into member two's construction and count opens and closes of the resource. The expected balance proves the cleanup path, though the test cannot substitute for reasoning about all possible throw sites.

The default constructor generation practice question is adjacent preparation for understanding how generated special members interact with initialization, but do not conflate default-constructor rules with exception guarantees.

Question 4: How can you implement a strong guarantee?

A common pattern is prepare, then commit. Build the candidate result in temporary storage without mutating the original. If allocation or copying throws, the temporary's RAII owners clean up and the original stays intact. Once preparation succeeds, commit through an operation known not to throw, such as an appropriate noexcept swap of representation. The Core Guidelines on swap explain why a non-failing swap is useful for strongly error-safe code.

For a resource-owning value type, copy-and-swap assignment is one illustration:

Widget& Widget::operator=(Widget other) {
    swap(other);          // only if this swap is genuinely noexcept
    return *this;
}

Constructing other may throw before the original changes. If the member swap really cannot throw, the commit succeeds. When other goes out of scope, it releases the old resources. The qualification matters: a swap that allocates or moves throwing members is not a safe commit merely because it has the name swap. Check allocator and member behavior rather than marking noexcept optimistically. If an exception escapes a noexcept function, the program terminates instead of getting a stronger rollback guarantee.

An exercise makes the distinction sharp. Your class has two vectors, a filename string, and a file write. Copying the vectors and string into a temporary can prepare in memory. Swapping those members may have conditions you must inspect. Writing the file before the swap creates an external effect that a failed swap cannot undo; writing it afterward means the object may commit while the file operation fails. You need a separate durable-file protocol, such as writing a temporary file and atomically replacing the destination where supported, to extend the strong contract beyond the object. Scope the claim instead of promising the world.

Question 5: Does std::vector always offer a strong guarantee on growth?

No universal answer is accurate. The draft vector modifiers wording gives no-effects behavior for specific insertion conditions and explicitly notes a case in which the effects are unspecified when a non-copy-insertable element's move constructor throws. The exception behavior depends on which operation is called, whether reallocation occurs, and the element type's copy and move properties. A thoughtful answer names those conditions rather than reciting “vector rolls back.”

Trace a simplified growth operation. It allocates a larger buffer, constructs elements there, and finally commits to the new storage. If construction of a new element throws before the old elements have been modified, it destroys completed new elements and releases the temporary buffer; the original remains usable. But if moving an existing element may throw after changing the source, the old buffer may no longer be unchanged. A copy route may preserve it when copying is supported; a noexcept move route can make relocation safe. That is why a type's move constructor and noexcept specification matter to generic containers.

Do not implement a vector by setting its size to the new value before all elements are constructed. If construction then throws, a destructor may iterate over uninitialized storage. Maintain separate “allocated capacity” and “constructed count,” clean up only completed elements, and update the externally visible size at the commit point. The simplified vector exception-safety practice question is especially relevant because it forces you to draw that state machine.

A worked trace: two exceptions in a resource-owning class

Suppose a class opens a file, allocates a buffer, and registers a callback. Failure A occurs while allocating the buffer. Failure B occurs after callback registration but before construction returns. If file ownership is already in a completed RAII member, failure A closes it. If the callback registration returns a detachable token owned by a guard, failure B unregisters it. If registration mutates a process-wide registry without such a guard, the partial side effect remains even though members unwind. The question is not “did C++ call destructors?” but “did every acquired resource have an already-constructed owner?”

Now imagine assignment to an existing object. Copy the candidate state first. If copying fails, the old object remains unchanged. If a no-throw swap commits it, the strong guarantee applies to that object's state. But if a callback was registered during preparation and remains visible to other threads, the externally observable state changed before the commit. Either delay registration, guard it with rollback, or narrow the promise. These two failure locations make a far better interview answer than the phrase “use smart pointers.”

To test this, make a controlled type throw on its second copy and count live instances. Assert the original value and live count after the exception. Then inject a failure after external registration and assert the registry is restored. A success-only test cannot prove failure guarantees. Run the test under a memory or address sanitizer if available, but understand that a clean sanitizer run on one path is evidence, not a mathematical proof of every path. Repair Unsafe C++ Ownership with Smart Pointers and Implement and Explain a C++ Smart Pointer offer focused ownership exercises.

How to answer the interviewer

Start with the operation's promised postcondition and the possible throw points. Mark when each resource gains an owner. Show the state before the first mutation and the commit boundary. For each throw point, walk backward through destructors and guards, and say what the caller can observe. Finally, qualify any strong guarantee by the element type, allocator, or external side effects involved.

That gives an answer an interviewer can challenge productively. It also prevents the two most common errors: assuming failed construction calls the failed object's destructor, and assuming RAII alone rolls back arbitrary state.

Sources and further reading