Modern C++ Fundamentals: From Basics to Smart Pointers and Threads · Ownership, Templates, Errors and Concurrency

Errors, optional, variant and Ranges

Handle errors with exceptions and noexcept, represent absent values with std::optional, model alternatives with std::variant and std::visit, and process data lazily with C++20 ranges.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A configuration reader that parses text values safely: missing values return std::optional, invalid input throws a meaningful exception, a std::variant holds a setting of several types, and a ranges pipeline filters and transforms results.

The idea

Exceptions report errors that can't be handled where they occur. Throw by value (throw std::invalid_argument("...")), catch by const&. Because RAII objects clean up during unwinding, exceptions and smart pointers work together. Mark functions that never throw noexcept.

std::optional<T> says "a value or nothing". Use it instead of sentinel values (-1, empty string) or null pointers: std::optional<int> parse_int(std::string_view). Check with if (opt), read with *opt or opt.value_or(0).

std::variant<A, B, C> holds exactly one of several types, safely. Use std::visit with a callable that handles each alternative, and the compiler makes sure you cover them all. This is C++'s sum type.

Ranges (C++20) let you pipe lazy views: numbers | std::views::filter(pred) | std::views::transform(f) | std::views::take(3). Nothing is computed until you iterate, and no intermediate containers are created.

In C++23, std::expected<T, E> adds a value-or-error return type (like Rust's Result). Where it isn't available, optional and exceptions cover most cases.

The code

optional, variant, exceptions and a ranges pipeline
#include <charconv>
#include <iostream>
#include <optional>
#include <ranges>
#include <stdexcept>
#include <string>
#include <string_view>
#include <variant>
#include <vector>

// optional: "maybe an int" without sentinel values
std::optional<int> parse_int(std::string_view text) {
    int value{};
    const auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value);
    if (ec != std::errc{} || ptr != text.data() + text.size()) return std::nullopt;
    return value;
}

// An exception for input that is present but wrong
int require_port(std::string_view text) {
    const auto port = parse_int(text);
    if (!port) throw std::invalid_argument("port is not a number: " + std::string{text});
    if (*port < 1 || *port > 65535) throw std::out_of_range("port out of range: " + std::to_string(*port));
    return *port;
}

// variant: a setting is one of three types
using Setting = std::variant<int, bool, std::string>;

std::string describe(const Setting& setting) {
    return std::visit(
        [](const auto& value) -> std::string {
            using T = std::decay_t<decltype(value)>;
            if constexpr (std::is_same_v<T, int>) return "int " + std::to_string(value);
            else if constexpr (std::is_same_v<T, bool>) return std::string{"bool "} + (value ? "true" : "false");
            else return "text '" + value + "'";
        },
        setting);
}

int main() {
    for (std::string_view input : {"8080", "80x", ""}) {
        const auto value = parse_int(input);
        std::cout << "'" << input << "' -> " << (value ? std::to_string(*value) : "invalid")
                  << " (or " << value.value_or(-1) << ")\n";
    }

    for (std::string_view input : {"443", "http", "70000"}) {
        try {
            std::cout << "port " << require_port(input) << " ok\n";
        } catch (const std::invalid_argument& e) {
            std::cout << "invalid: " << e.what() << '\n';
        } catch (const std::exception& e) {
            std::cout << "error: " << e.what() << '\n';
        }
    }

    const std::vector<Setting> settings{42, true, std::string{"dark"}};
    for (const auto& s : settings) std::cout << describe(s) << '\n';

    // A lazy ranges pipeline: square the odd numbers, keep the first three
    const std::vector<int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto result = numbers
                  | std::views::filter([](int n) { return n % 2 == 1; })
                  | std::views::transform([](int n) { return n * n; })
                  | std::views::take(3);
    for (int n : result) std::cout << n << ' ';
    std::cout << '\n';
}

Output

'8080' -> 8080 (or 8080)
'80x' -> invalid (or -1)
'' -> invalid (or -1)
port 443 ok
port invalid: port is not a number: http
port error: port out of range: 70000
int 42
bool true
text 'dark'
1 9 25 

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Catching exceptions by value. It slices derived exceptions; catch by const&.
  • Throwing for normal control flow. Exceptions are for exceptional situations; use optional for "not found".
  • Dereferencing an empty optional. *opt on an empty optional is undefined behavior; check it or use value_or.
  • Using std::get<T> on the wrong alternative. It throws bad_variant_access; prefer std::visit or std::holds_alternative.
  • Keeping a view after its source is gone. Views don't own their data; the vector must outlive them.

Key points

  • Use exceptions for errors, optional for "maybe a value", and variant + visit for alternatives.
  • RAII makes exception handling safe; mark non-throwing functions noexcept.
  • Ranges views are lazy pipelines (filter, transform, take) that avoid temporary containers.
  • std::expected (C++23) adds an explicit value-or-error type.

Further reading: cppreference: std::optional and Ranges library.

Practice questions

Why prefer `std::optional<int>` to returning -1 for "not found"?

Answer

A sentinel looks like a valid value and callers can forget to check it. optional makes the possibility of no value part of the type, so the compiler and readers see it, and value_or or an explicit check handles the empty case.

What does `std::visit` give you over manually checking a variant's type?

Answer

It dispatches to a handler for whichever alternative is stored and requires the visitor to handle every alternative, so adding a new type to the variant produces a compile error wherever it isn't handled.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?