Using C++20 and C++23

Using C++20 and C++23

Start adopting modern language and library features that pay off quickly.

Using C++20 and C++23

High-value features to adopt early

These are good first targets because they usually improve readability immediately without demanding a full architecture rewrite.

Example: std::span

void print_all(std::span<const int> values) {
    for (int value : values) {
        std::cout << value << '\n';
    }
}

std::span lets one function accept arrays, vectors, and other contiguous ranges without taking ownership.

Example: concepts

template <typename T>
concept Printable = requires(T value) {
    std::cout << value;
};

Concepts turn implicit template assumptions into explicit API rules, which is one of the highest-value changes in modern generic code.

Adoption strategy

Add modern features where they improve readability immediately. Do not rewrite stable code just to chase newer syntax.

Fast wins in ordinary code

Replace pointer-plus-size pairs with std::span

void process(std::span<const std::byte> bytes);

Replace manual thread ownership with std::jthread

std::jthread worker([](std::stop_token token) {
    while (!token.stop_requested()) {
        poll_once();
    }
});

Replace ad hoc parse status codes with std::expected

std::expected<int, std::string> parse_count(std::string_view text) {
    if (text.empty()) {
        return std::unexpected("empty input");
    }
    return 42;
}

More library features worth knowing

Example: three-way comparison

struct Version {
    int major{};
    int minor{};
    auto operator<=>(const Version&) const = default;
};

With a defaulted <=>, the compiler can synthesize the normal relational operators for you. This is one of the cleaner modern features because it removes repetitive comparison boilerplate.

Example: std::source_location

void log_message(std::string_view text,
                 std::source_location where = std::source_location::current()) {
    std::cout << where.file_name() << ':' << where.line() << " " << text << '\n';
}

This is a practical upgrade over hand-passing file and line macros through your own logging APIs.

Feature selection by problem

Upgrade by layer

The cleanest rollout is usually bottom-up.

This keeps the early changes cheap and local instead of mixing low-risk upgrades with toolchain-heavy ones.

Before-and-after style upgrades

Old pointer-plus-size

void process(const int* values, std::size_t count);

New borrowed range

void process(std::span<const int> values);

Old manual thread join

std::thread worker(run);
worker.join();

New owned thread

std::jthread worker(run);

Example: std::expected

std::expected<int, std::string> parse_count(std::string_view text) {
    if (text.empty()) {
        return std::unexpected("empty input");
    }
    return 42;
}

The point is not that expected replaces every exception. It gives you a strong option when failure is part of normal control flow and callers should branch on the result directly.

Modules and coroutines

These two features are real parts of modern C++, but they require more build-system and library support than features like std::span or concepts. Adopt them deliberately rather than mechanically.

Where expected does and does not fit

std::expected works best when callers are supposed to branch locally on failure.

It is usually a worse fit when every caller would just forward the error upward, or when the failure is exceptional enough that unwinding is the clearer design.

That means expected and exceptions often coexist in the same codebase. Use each where its calling pattern is natural.

Practical rollout order

  1. std::string_view, std::span, [[nodiscard]]
  2. concepts, ranges, std::format
  3. std::jthread, std::expected, std::source_location
  4. modules and coroutines once toolchain and surrounding libraries are ready

Exercise ideas

Practical migration checklist

Example in practice

#include <format>
#include <string>

int main() {
    std::string text = std::format("value = {}", 42);
    return static_cast<int>(text.size());
}

Try this variation

Swap one older pattern for a newer standard facility, then compare clarity. This keeps modern C++ grounded in practical tradeoffs instead of feature tourism.

#include <format>
#include <string>

int main() {
    std::string line = std::format("{} items ready", 3);
    return static_cast<int>(line.size());
}