xSKILLby Infirals
Register

C++ for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

Exceptions: throw, catch, unwind

Exceptions, STL, and ownership13 min readLesson 1 of 4

1.1 Errors that need a different path

A function can report failure with a return code, and callers can ignore it. A constructor cannot return a status at all, and a chain of calls can fail several levels below the code that knows how to respond. Exceptions move the failure to a handler without threading an error value through every function.

A throw creates an exception object and transfers control to the nearest matching catch. Code between the throw and the handler does not run to completion; its local objects are destroyed as the stack unwinds. The handler then decides whether the program can continue.

cpp
#include <iostream>
#include <stdexcept>

int checked_age(int age) {
    if (age < 0) {
        throw std::runtime_error("age cannot be negative");
    }
    return age;
}
int main() {
    try {
        std::cout << checked_age(20) << "\n";
        std::cout << checked_age(-1) << "\n";
    } catch (const std::runtime_error& error) {
        std::cout << "Error: " << error.what() << "\n";
    }
    return 0;
}

Output

20
Error: age cannot be negative

1.1.1 Matching a handler

A try block marks code whose exceptions this handler covers. Each catch names a type, and the runtime selects the first handler whose type matches the exception or a base of it. Order the handlers from the most specific type to the most general, and put catch (...) last.

Catch by const reference. Catching by value copies the exception and can slice a derived exception into its base type. Throwing by value and catching by reference is the usual pairing. An exception with no matching handler calls std::terminate, so a program that throws must have a plan for it.

cpp
#include <iostream>
#include <stdexcept>
class Guard {
public:
    explicit Guard(const char* name) : name_(name) { std::cout << "enter " << name_ << "\n"; }
    ~Guard() { std::cout << "leave " << name_ << "\n"; }
private:
    const char* name_;
};
void run() {
    Guard guard("run");
    throw std::runtime_error("stop");
}
int main() {
    try { run(); }
    catch (const std::exception&) { std::cout << "caught\n"; }
    return 0;
}

Output

enter run
leave run
caught

1.1.2 Exceptions during construction

A constructor that detects invalid arguments can throw. The object is then considered never constructed, so its destructor does not run. Data members that were fully constructed before the throw are destroyed, and base classes are destroyed in reverse order.

Raw resources acquired in the constructor body before the throw can leak, because no destructor cleans them up. Members that own resources through RAII solve that problem: their destructors run during unwinding. Prefer std::string, std::vector, and smart pointers as members so a failed constructor leaves nothing behind.

cpp
#include <iostream>
#include <stdexcept>
class Account {
public:
    explicit Account(int balance) : balance_(balance) { if (balance_ < 0) throw std::invalid_argument("negative balance"); }
    int balance() const { return balance_; }
private:
    int balance_;
};
int main() {
    try {
        Account good(50);
        std::cout << "Balance: " << good.balance() << "\n";
        Account bad(-5);
    } catch (const std::invalid_argument& error) {
        std::cout << "Rejected: " << error.what() << "\n";
    }
    return 0;
}

Output

Balance: 50
Rejected: negative balance

1.1.3 Standard exception types

std::exception is the base class for the standard exception hierarchy. std::runtime_error covers errors detected at runtime, std::logic_error covers programming mistakes, and std::invalid_argument and std::out_of_range are derived from logic_error. std::vector::at and std::string::substr throw std::out_of_range when a position is invalid.

A custom exception can derive from std::runtime_error, pass a message to its constructor, and add data such as an error code. It can then be caught by its own type or by std::exception. Keep the message short and put machine-readable details in members.

cpp
#include <iostream>
#include <stdexcept>
#include <vector>

int main() {
    std::vector<int> marks = {70, 80};
    try {
        std::cout << "Mark: " << marks.at(1) << "\n";
        int missing = marks.at(5);
        std::cout << "Mark: " << missing << "\n";
    } catch (const std::out_of_range&) {
        std::cout << "Index out of range\n";
    }
    return 0;
}

Output

Mark: 80
Index out of range

1.1.4 A custom exception type

Deriving from a standard exception keeps the catch sites familiar and lets what() return a message. The derived constructor forwards the message to the base constructor and initializes its own fields in the initializer list.

Catch the custom type by const reference when the handler needs its extra data, and catch std::exception at a boundary that only logs or reports. A class used as an exception should be copyable, because the runtime may copy the thrown object.

cpp
#include <iostream>
#include <stdexcept>

class InsufficientFunds : public std::runtime_error {
public:
    explicit InsufficientFunds(int shortfall)
        : std::runtime_error("insufficient funds"), shortfall_(shortfall) {}
    int shortfall() const { return shortfall_; }
private:
    int shortfall_;
};
int main() {
    try {
        throw InsufficientFunds(30);
    } catch (const InsufficientFunds& error) {
        std::cout << error.what() << ": " << error.shortfall() << "\n";
    }
    return 0;
}

Output

insufficient funds: 30

1.1.5 Exception safety and destructors

Code that throws leaves objects in some state. The basic guarantee says the program remains valid and resources are released. The strong guarantee says a failed operation leaves the object unchanged, which usually means doing the work on a copy and swapping at the end.

Destructors should not throw. They often run during unwinding, and a second active exception calls std::terminate. The standard library marks most destructors noexcept for this reason. Use RAII so cleanup does not depend on a catch block running.

1.1.6 When an exception is the wrong tool

An exception reports something unexpected that prevents an operation from finishing. A search that finds no result, an optional setting that is absent, or a user who cancels an action are ordinary outcomes, and they read better as a return value. C++17 offers std::optional<T> for a value that may be absent.

An exception also costs more than a returned value, and a handler far from the throw can hide the control flow. Use exceptions at boundaries where the failure cannot be handled locally, and document the exceptions a function may throw. For functions that never throw, noexcept lets the compiler and the standard library optimize around that fact.

1.1.7 Common mistakes and practice

Mistakes to watch for:

  • Catching by value, which can slice a derived exception.
  • Swallowing every exception with an empty catch (...) block.
  • Throwing a pointer to a local object.
  • Letting a destructor throw during unwinding.
  • Using exceptions for ordinary branching such as a missing optional value.

Practice:

  1. Write a function that throws std::runtime_error when a number is negative and catch it in main.
  2. Print from a local object's destructor to watch stack unwinding.
  3. Make a constructor reject a negative balance and catch the failure in main.
  4. Catch std::out_of_range from std::vector::at.
  5. Add a custom exception with an error code and print both the message and the code.

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01Why catch exceptions by const reference?

    Catching by value copies the exception object and can slice a derived exception into its base type. A const reference binds to the original object and preserves its dynamic type.

  • 02What happens to local objects when an exception leaves a function?

    The runtime destroys every fully constructed local object in reverse order of construction as the stack unwinds. This is why RAII classes release resources even when an exception is thrown.

  • 03Why should a destructor avoid throwing exceptions?

    A destructor often runs during stack unwinding. If it throws while another exception is active, the program calls std::terminate. Destructors should catch and handle their own errors.

Quiz

Check: C++ exceptions

Four questions on throwing, matching handlers, stack unwinding and exceptions during construction.

4 questions

Question 1 of 4

0/4 answered

Catching an exception by value risks:

Answer all you can, then submit.