Kodewick
Lesson 26 of 30Contents

Exceptions

Lesson 26 of 30 · Part 6 · Inheritance and templates · about 20 minutes

When a function goes wrong, how does it tell whoever called it? The old way is to return a special value, like "return 0 if it can't be worked out". But what if 0 is also a normal answer?

main.cpp
1#include <iostream>2#include <vector>3 4double average(const std::vector<double>& temps) {5    if (temps.empty()) {6        return 0;7    }8    double total = 0;9    for (double t : temps) {10        total += t;11    }12    return total / temps.size();13}14 15int main() {16    std::vector<double> winter{-3, 1, 2};17    std::vector<double> nothing;18    std::cout << "Winter average: " << average(winter) << '\n';19    std::cout << "No data: " << average(nothing) << '\n';20    return 0;21}
Output
Winter average: 0
No data: 0

Two zeros: one is a real average temperature, the other means "there's no data at all", and the caller can't tell them apart. Even if it could, it would have to remember to check after every call. C++ has another way, exceptions: when something goes wrong, the function doesn't return; it "throws" an exception, handing the problem to someone who knows what to do.

throw and try / catch

main.cpp
1#include <iostream>2#include <stdexcept>3#include <vector>4 5double average(const std::vector<double>& temps) {6    if (temps.empty()) {7        throw std::runtime_error("no data");8    }9    double total = 0;10    for (double t : temps) {11        total += t;12    }13    return total / temps.size();14}15 16int main() {17    std::vector<double> nothing;18    try {19        double avg = average(nothing);20        std::cout << "Average: " << avg << '\n';21    } catch (const std::runtime_error& e) {22        std::cout << "Something went wrong: " << e.what() << '\n';23    }24    std::cout << "The program goes on\n";25    return 0;26}
Output
Something went wrong: no data
The program goes on

Three new things:

  • throw std::runtime_error("no data"); creates an exception object and throws it. average stops right there and returns no value. std::runtime_error is in <stdexcept>; the text in parentheses says what went wrong.
  • try { … } surrounds code that might go wrong.
  • catch (const std::runtime_error& e) { … } follows the try and catches that kind of exception. e.what() gives back the text written when it was thrown.

After the throw, the rest of the try is skipped and control goes straight to the catch; once the catch is done, the program carries on after the whole try / catch. So the "Average: " line doesn't show, and "The program goes on" does.

When nobody catches it

If nobody catches it, the exception keeps going outward, out of main, and the program ends:

main.cpp
1#include <iostream>2#include <stdexcept>3#include <vector>4 5double average(const std::vector<double>& temps) {6    if (temps.empty()) {7        throw std::runtime_error("no data");8    }9    double total = 0;10    for (double t : temps) {11        total += t;12    }13    return total / temps.size();14}15 16int main() {17    std::vector<double> nothing;18    double avg = average(nothing);19    std::cout << "Average: " << avg << '\n';20    std::cout << "End\n";21    return 0;22}
Output
terminate called after throwing an instance of 'std::runtime_error'
  what():  no data
Aborted (core dumped)

What happened: An exception (std::runtime_error) was thrown and no catch took it, so the program ended (std::terminate).

These three lines are what a g++ program shows on your own computer too: terminate called after throwing an instance of 'std::runtime_error' says a std::runtime_error was thrown and nobody caught it, so std::terminate was called; after what(): comes the text; Aborted (core dumped) means the program was forced to stop. "End" never shows.

An exception can't be ignored: either it's caught, or the program ends. That beats returning a 0 that anyone might forget to check.

The standard library throws too

Lesson 11 said v.at(5) out of range throws std::out_of_range, and the program ends if nothing handles it. Lesson 5's std::stoi throws std::invalid_argument when the text isn't a number. Now you can catch them:

main.cpp
1#include <iostream>2#include <stdexcept>3#include <string>4#include <vector>5 6int main() {7    std::vector<int> prices{12, 30, 8};8    try {9        std::cout << prices.at(5) << '\n';10    } catch (const std::exception& e) {11        std::cout << "Bad index: " << e.what() << '\n';12    }13    try {14        int n = std::stoi("twelve");15        std::cout << n << '\n';16    } catch (const std::exception& e) {17        std::cout << "Not a number: " << e.what() << '\n';18    }19    std::cout << "Both errors caught\n";20    return 0;21}
Output
Bad index: vector::_M_range_check: __n (which is 5) >= this->size() (which is 3)
Not a number: stoi
Both errors caught

This catches const std::exception&. std::exception is the base class of every standard exception (inheritance, lesson 23): std::runtime_error, std::out_of_range and std::invalid_argument are all derived from it, so one catch (const std::exception& e) catches them all, and e.what() calls each one's own version (virtual functions, lesson 24). The what() from stoi is short, just stoi.

Catch by reference (&): no copy, and a derived object isn't sliced down to the base. Add const, since you're only looking. That's the usual rule for catching.

Several catches

A try can be followed by several catches, each doing something different for a different error. When an exception is thrown, they're checked top to bottom, and the first one that matches catches it; the rest are ignored:

main.cpp
1#include <iostream>2#include <stdexcept>3#include <string>4#include <vector>5 6int seat_number(const std::string& text) {7    int n = std::stoi(text);8    if (n < 1 || n > 30) {9        throw std::runtime_error("no such seat");10    }11    return n;12}13 14int main() {15    std::vector<std::string> tickets{"12", "abc", "45"};16    for (const auto& t : tickets) {17        try {18            int seat = seat_number(t);19            std::cout << "Seat " << seat << '\n';20        } catch (const std::invalid_argument& e) {21            std::cout << t << " is not a number\n";22        } catch (const std::runtime_error& e) {23            std::cout << t << ": " << e.what() << '\n';24        }25    }26    return 0;27}
Output
Seat 12
abc is not a number
45: no such seat

"abc" makes stoi throw invalid_argument, caught by the first catch; "45" makes seat_number itself throw runtime_error ("no such seat" is this program's own rule, so it uses runtime_error), which the first doesn't match, so the second catches it. The try is inside the loop body, so one bad ticket doesn't stop the next.

Since the first match wins, put specific catches first and catch-everything ones like std::exception last. There's also catch (...), with three dots, which catches any type but gives you no what(); it can only be the last one.

Guess first

What does this program show?

1#include <iostream>2#include <stdexcept>3 4void check(int n) {5    std::cout << "a";6    if (n > 2) {7        throw std::runtime_error("too big");8    }9    std::cout << "b";10}11 12int main() {13    try {14        check(1);15        check(5);16        check(2);17        std::cout << "c";18    } catch (const std::exception& e) {19        std::cout << "d";20    }21    std::cout << "e\n";22    return 0;23}

Objects on the way are destroyed

An exception jumps from throw to catch, maybe across several layers of functions. What about the local objects in those layers? C++ guarantees that as each layer is left, its local objects are destroyed, just as when a function ends normally. This is called stack unwinding.

main.cpp
1#include <iostream>2#include <stdexcept>3#include <string>4 5class Cup {6public:7    Cup(const std::string& name) : name_(name) {8        std::cout << "Take out the " << name_ << '\n';9    }10    ~Cup() {11        std::cout << "Put away the " << name_ << '\n';12    }13 14private:15    std::string name_;16};17 18void make_tea() {19    Cup cup{"teacup"};20    throw std::runtime_error("not enough water");21    std::cout << "Pour the tea\n";22}23 24int main() {25    try {26        Cup tray{"tray"};27        make_tea();28    } catch (const std::exception& e) {29        std::cout << "Caught: " << e.what() << '\n';30    }31    return 0;32}
Output
Take out the tray
Take out the teacup
Put away the teacup
Put away the tray
Caught: not enough water

Press Step, and after the throw on line 20, go one step at a time: the next step isn't the catch but the destructor of cup in make_tea; then the destructor of tray in main's try; only then the catch. The teacup is put away before the tray, the reverse of the order they were made.

That's why lesson 21's smart pointers and lesson 11's vector don't leak when an exception passes through: they're local objects, and their destructors still run.

Your own exception class

The standard exception classes are often enough, but sometimes you want the exception to carry more, or want a catch to tell "this is a bank error". Write a class that inherits from std::runtime_error:

main.cpp
1#include <iostream>2#include <stdexcept>3#include <string>4 5class BankError : public std::runtime_error {6public:7    BankError(const std::string& message, int short_by)8        : std::runtime_error(message), short_by_(short_by) {}9    int short_by() const {10        return short_by_;11    }12 13private:14    int short_by_;15};16 17class Account {18public:19    Account(int balance) : balance_(balance) {}20    void withdraw(int amount) {21        if (amount > balance_) {22            throw BankError("not enough money", amount - balance_);23        }24        balance_ -= amount;25        std::cout << "Took out " << amount << ", " << balance_ << " left\n";26    }27 28private:29    int balance_;30};31 32int main() {33    Account account{100};34    try {35        account.withdraw(30);36        account.withdraw(100);37    } catch (const BankError& e) {38        std::cout << e.what() << ", short by " << e.short_by() << '\n';39    } catch (const std::exception& e) {40        std::cout << "Other error: " << e.what() << '\n';41    }42    return 0;43}
Output
Took out 30, 70 left
not enough money, short by 30

BankError's constructor calls the base class's constructor std::runtime_error(message) in its initializer list (lesson 23), and that gives it what(); it adds its own member short_by_ for how much is missing. Because it's derived from std::runtime_error, code elsewhere that only has catch (const std::exception& e) catches it too.

Note: throw can actually be followed by any value: throw 42; or throw std::string("oops"); both work. But then there's no what(), and catch (const std::exception&) won't catch it. By habit, throw only standard exceptions or classes derived from them.

When to use exceptions

Exceptions suit errors that "can't be handled here and shouldn't happen": the function can't do what it promised and doesn't know what to do, so it passes the problem up. Constructors are the classic place. A constructor has no return value, so when its arguments are wrong, throwing is the only way out, and that way there's never a half-broken object:

main.cpp
1#include <iostream>2#include <stdexcept>3 4class Discount {5public:6    Discount(int percent) : percent_(percent) {7        if (percent < 0 || percent > 100) {8            throw std::invalid_argument("discount must be from 0 to 100");9        }10    }11    double apply(double price) const {12        return price * (100 - percent_) / 100;13    }14 15private:16    int percent_;17};18 19int main() {20    try {21        Discount sale{20};22        std::cout << sale.apply(50) << '\n';23        Discount wrong{120};24        std::cout << wrong.apply(50) << '\n';25    } catch (const std::invalid_argument& e) {26        std::cout << e.what() << '\n';27    }28    return 0;29}
Output
40
discount must be from 0 to 100

wrong is never created, so nothing gets the chance to use a bad discount.

SituationUse
Common, and the caller deals with it at once (not found, bad input)a return value, like find returning end()
Rare, and can't be handled here (bad arguments, a resource ran out)throw an exception
A bug in the program (an index worked out wrong)fix the code; don't hide it with catch

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it. Checking compares only the output, so in these exercises the catch shows a line, proving it really caught something.

Exercise 1

Catch the bad index

The second order uses index 7, so menu.at(7) throws and the program ends. Add a try / catch in the loop body that catches std::out_of_range and shows No index 7. The program should show these two lines.

It should print
Tea
No index 7
exercise1.cpp
1#include <iostream>2#include <stdexcept>3#include <string>4#include <vector>5 6int main() {7    std::vector<std::string> menu{"Coffee", "Tea", "Juice"};8    std::vector<int> picks{1, 7};9    for (int i : picks) {10        std::cout << menu.at(i) << '\n';11    }12    return 0;13}
Output
Press Run or Ctrl+Enter
Exercise 2

No dividing by zero

main is ready to catch, but divide doesn't throw on 0. Make divide throw a std::runtime_error with the text can't divide by 0 when b is 0. The program should show these two lines.

It should print
5
Error: can't divide by 0
exercise2.cpp
1#include <iostream>2#include <stdexcept>3#include <vector>4 5int divide(int a, int b) {6    return a / b;7}8 9int main() {10    std::vector<int> divisors{2, 0};11    for (int b : divisors) {12        try {13            std::cout << divide(10, b) << '\n';14        } catch (const std::runtime_error& e) {15            std::cout << "Error: " << e.what() << '\n';16        }17    }18    return 0;19}
Output
Press Run or Ctrl+Enter
Exercise 3

Fix it: the specific catch goes first

"eight" isn't a number, so this should show Not a number: eight, but it shows Something went wrong, and the compiler gives a warning. Only change the order of the code, so the program shows these two lines.

It should print
16
Not a number: eight
exercise3.cpp
1#include <iostream>2#include <stdexcept>3#include <string>4#include <vector>5 6int main() {7    std::vector<std::string> inputs{"8", "eight"};8    for (const auto& text : inputs) {9        try {10            int n = std::stoi(text);11            std::cout << n * 2 << '\n';12        } catch (const std::exception& e) {13            std::cout << "Something went wrong\n";14        } catch (const std::invalid_argument& e) {15            std::cout << "Not a number: " << text << '\n';16        }17    }18    return 0;19}
Output
Press Run or Ctrl+Enter

What you learned

  • When something goes wrong, a function can throw an exception, like throw std::runtime_error("no data"); (<stdexcept>), instead of returning a special value that's easy to mix up.
  • try { … } surrounds code that might fail, catch (const std::runtime_error& e) catches it, and e.what() is the text; after the catch, the program carries on after the try / catch.
  • If nobody catches it, the program ends: terminate called after throwing an instance of …, what():, Aborted (core dumped).
  • v.at() and std::stoi throw on their own (out_of_range, invalid_argument), and now you can catch them.
  • catch (const std::exception& e) catches every standard exception; with several catches the first match wins, so specific ones go first and catch (...) goes last.
  • After a throw, the local objects in every layer between throw and catch are destroyed (stack unwinding).
  • Your own exception class inherits from std::runtime_error, with a constructor that says : std::runtime_error(message).
  • Use exceptions for rare errors that can't be handled where they happen; use return values for common cases; fix bugs in the code.