Step 6 of 7
Exceptions
Exceptions are standard C++'s general tool for failure, and most professional codebases use them. Everything in this step runs in this site's compiler too.
throw, try and catch
#include <iostream>
#include <stdexcept>
#include <string>
int parse_percent(const std::string& s) {
int v = std::stoi(s); // throws std::invalid_argument for "abc"
if (v < 0 || v > 100) throw std::out_of_range("percent must be 0 to 100: " + s);
return v;
}
int main() {
for (std::string s : {"42", "150", "abc"}) {
try {
std::cout << parse_percent(s) << "\n";
} catch (const std::out_of_range& e) {
std::cout << "range error: " << e.what() << "\n";
} catch (const std::exception& e) {
std::cout << "other error: " << e.what() << "\n";
}
}
}
42
range error: percent must be 0 to 100: 150
other error: stoi
throwcreates an exception object and abandons the current function immediately. Control jumps to the nearest enclosingtryblock that has a matchingcatch, skipping everything in between.parse_percentdoesn't need anifto pass the failure up: that happens automatically.catchclauses are tried in order, and a handler for a base class also catches every derived class. So list specific types first:std::out_of_rangebeforestd::exception, which is the base of all standard exceptions.e.what()returns the message. The standard library's own messages differ between implementations: GCC'sstd::stoijust saysstoi(the output above), while this site's library, libc++, saysstoi: no conversion.- If nothing catches an exception, the program calls
std::terminateand dies. A Linux build printsterminate called after throwing an instance of 'std::runtime_error'and thewhat()message, and this site shows the same.
Stack unwinding runs destructors
On the way from the throw to the catch, every local object in every abandoned function is destroyed, in reverse order. That's why RAII matters so much: cleanup happens even when a function is left by an exception.
#include <iostream>
#include <stdexcept>
#include <string>
struct Guard {
std::string name;
explicit Guard(std::string n) : name(n) { std::cout << "open " << name << "\n"; }
~Guard() { std::cout << "close " << name << "\n"; }
};
void load() {
Guard file("config.txt");
Guard lock("cache lock");
throw std::runtime_error("disk read failed");
}
int main() {
try {
load();
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n";
}
}
open config.txt
open cache lock
close cache lock
close config.txt
caught: disk read failed
The rules professionals follow
- Throw by value, catch by
const&. Catching by value copies the exception and slices derived types down to the base. - Derive your own types from the standard ones, usually
std::runtime_error, and add the details a handler needs:struct ParseError : std::runtime_error { int line; ... };. throw;(with nothing after it) inside acatchre-throws the same exception, after logging or partial cleanup.- Destructors must not throw. They're
noexceptby default; a destructor that throws during unwinding terminates the program. - Mark moves and swaps
noexceptso containers can use them safely (the move semantics module showed why). - Exceptions cost almost nothing when nothing is thrown, with the compilers used for desktops and servers, but throwing one is slow. Use them for genuinely exceptional failures, not for ordinary control flow like "key not found".
Exception safety
When a function fails halfway, what state is left behind? The guarantees, from weakest to strongest:
- Basic: nothing leaks and every object is still valid, but values may have changed.
- Strong: all or nothing. If the operation fails, it's as if it never started.
- No-throw: it can't fail (
noexcept).
The standard recipe for the strong guarantee works with or without exceptions: do all the work on a copy, and only when everything has succeeded, swap the copy into place. Swapping can't fail, so if anything throws along the way, the original is untouched.
Your turn: write TransferError, an exception type derived from std::runtime_error, and apply_all, which applies a list of transfers between accounts in order, with the strong guarantee. If transfer number k (counting from 1) uses an account that doesn't exist, throw TransferError("transfer k: unknown account"); if it would make a balance negative, throw TransferError("transfer k: insufficient funds"). Either way balances must be left exactly as it was.