RAII and exception guarantees
While an exception flies down, every floor it passes closes, and the lamps of its local objects switch off by themselves: destructors run during stack unwinding. But an object from new has no such switch. That lamp is switched off only by hand, with delete, and execution may never reach that line.
An exception is one more way out of a function, and resources must be released on that path too. Stack unwinding runs the destructors of local objects, but a plain pointer has no destructor that would delete its object:
#include <iostream>
#include <memory>
#include <stdexcept>
struct Noisy { ~Noisy() { std::cout << "~Noisy "; } };
int main() {
try {
Noisy* raw = new Noisy; // ✗
auto owned = std::make_unique<Noisy>();
throw std::runtime_error("fail");
delete raw;
} catch (const std::exception& e) {
std::cout << e.what() << '\n'; // ~Noisy fail
}
}Let's see why the destructor ran once, although there are two Noisy objects:
rawgets the address of the firstNoisyon the heap.ownedowns the secondNoisythrough astd::unique_ptr.throwcuts thetryblock short, and execution never reachesdelete raw.- During stack unwinding
ownedis destroyed and deletes its object:~Noisyis printed. - The first
Noisystays on the heap—a leak.
An owner on every path
So that an exception leaves no leaks, resources are handed to RAII owners such as std::unique_ptr instead of wrapping each delete in a try block. An owner's destructor runs on every path out of a function, an exception included, as long as some handler catches it.
Three levels of guarantees
In case of an exception, a function can give one of three guarantees. Interviewers often ask about them, so let's go through each one.
Basic guarantee
Basic guarantee: nothing leaks, and all objects stay usable, although their values may have changed. RAII owners give the first part of the basic guarantee: an exception causes no leaks.
Strong guarantee
Strong guarantee: if the call ends with an exception, the program's state is the same as before the call. It is usually achieved like this: the new value is built in a local variable and handed out only at the end.
void add_pair(std::vector<int>& v, int a, int b) {
std::vector<int> copy = v;
copy.push_back(a);
copy.push_back(b);
v = std::move(copy);
}Line by line:
copyis a copy of the vectorv, and from here on the function changes only the copy.- Any
push_backmay throw an exception if memory runs out. Then stack unwinding destroyscopy, andvstays as it was. v = std::move(copy)moves the finished result intovand throws no exceptions.
If the function added the elements straight into v, an exception in the second push_back would leave only a there. That is the basic guarantee, but not the strong one.
No-throw guarantee
No-throw guarantee: the function never throws an exception at all. This promise is written in the function's declaration with the word noexcept.
Key points
- An exception is one more way out of a function, and only a destructor releases resources on that path.
- An object from
newwithout an owner leaks if an exception skipsdelete; an RAII owner releases it by itself. - Basic guarantee: no leaks, objects stay usable; strong: after an exception the state is unchanged.
- The strong guarantee comes from building the result in a copy and handing it out at the end.
Subscribe to take the course
The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.