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.
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
#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
optionalfor "not found". - Dereferencing an empty
optional.*opton an empty optional is undefined behavior; check it or usevalue_or. - Using
std::get<T>on the wrong alternative. It throwsbad_variant_access; preferstd::visitorstd::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,
optionalfor "maybe a value", andvariant+visitfor 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.