पाठ 19 / 25

Exceptions, noexcept and std::expected

Choose between exceptions and error values, and write exception-safe code.

Reporting failure

C++ supports exceptions: throw an object (usually derived from std::exception, such as std::invalid_argument or std::runtime_error), and try/catch it higher up, catching by const reference (catch (const std::exception& e)). During stack unwinding, destructors of all automatic objects run, which is why RAII makes exception handling safe. Exception-safety guarantees describe functions: no-throw (marked noexcept, important for move constructors and destructors, which containers rely on), strong (an operation either succeeds or leaves state unchanged) and basic (no leaks, valid state). Exceptions suit rare, truly exceptional failures. For expected failures such as parsing user input, many codebases return values instead: std::optional when there is no detail, and std::expected<T, E> (C++23) to return either a value or an error. Some domains, such as games and embedded systems, compile with exceptions disabled and use error codes throughout. Whatever the style, be consistent within a codebase and never let errors pass silently.

Stack unwinding

When an exception is thrown, frames are unwound and their destructors run until a matching handler is found.

A stack of frames with a lightning bolt at the top frame and arrows peeling frames away downward until reaching a frame with a net icon.
Figure 7.1 — Exception propagation and destructor calls during unwinding.

Exceptions for exceptional cases, expected for expected ones

Both styles side by side.

#include <expected>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>

std::string read_config(const std::string& path) {
    std::ifstream in(path);
    if (!in) throw std::runtime_error("cannot open " + path);    // exceptional: config must exist
    return {std::istreambuf_iterator<char>(in), {}};
}

enum class ParseError { Empty, NotANumber, OutOfRange };

std::expected<int, ParseError> parse_age(const std::string& s) {   // expected failures
    if (s.empty()) return std::unexpected(ParseError::Empty);
    int v = 0;
    for (char c : s) {
        if (c < '0' || c > '9') return std::unexpected(ParseError::NotANumber);
        v = v * 10 + (c - '0');
        if (v > 150) return std::unexpected(ParseError::OutOfRange);
    }
    return v;
}

int main() {
    try {
        auto cfg = read_config("app.conf");
    } catch (const std::exception& e) {
        // log e.what() and exit with an error code
    }
    if (auto age = parse_age("34"); age) { /* *age == 34 */ }
    else if (age.error() == ParseError::NotANumber) { /* tell the user */ }
}

Mark move operations noexcept

std::vector uses a type's move constructor during reallocation only if it is noexcept; otherwise it copies to preserve the strong guarantee. Forgetting noexcept on moves can silently make code much slower.

त्वरित जाँच: How should exceptions usually be caught in C++?

  • By const reference
  • By value
  • By pointer
  • With catch(...) only
Answer

By const reference — Catching by const reference avoids slicing and unnecessary copies.