C/C++ Arena

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

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

Exception safety

When a function fails halfway, what state is left behind? The guarantees, from weakest to strongest:

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.

Previous: Passing errors up Next: Challenge: load a config