Skip to main content
Technology & EngineeringSystems Programming294 lines

Cpp Modern

Modern C++ (C++20/23) patterns including concepts, ranges, coroutines, and modules

Quick Summary18 lines
You are an expert in Modern C++ (C++20/23) for writing systems-level code.

## Key Points

- Prefer `std::expected` or error codes over exceptions in performance-critical or embedded code paths.
- Use concepts to constrain templates; they produce clearer error messages than SFINAE.
- Prefer `std::span<T>` over raw pointer + size pairs for function parameters.
- Use `constexpr` and `consteval` to move computation to compile time wherever possible.
- Prefer `std::string_view` over `const std::string&` for non-owning string parameters.
- Use `std::move` semantics and pass sink parameters by value to enable moves.
- Adopt modules for new code to reduce compile times and improve encapsulation.
- Dangling references from `std::string_view` pointing to temporary strings that go out of scope.
- Forgetting that `std::ranges::views` are lazy and invalidated if the underlying container mutates.
- Coroutine lifetime bugs: the coroutine frame is heap-allocated and must outlive all references to its state.
- Using `std::shared_ptr` where `std::unique_ptr` suffices adds unnecessary atomic reference counting overhead.
- Misunderstanding `constexpr` vs `consteval`: `constexpr` permits runtime evaluation; `consteval` does not.
skilldb get systems-programming-skills/Cpp ModernFull skill: 294 lines
Paste into your CLAUDE.md or agent config

Modern C++ — Systems Programming

You are an expert in Modern C++ (C++20/23) for writing systems-level code.

Core Philosophy

Overview

C++20 and C++23 introduce transformative features that modernize the language while preserving zero-cost abstractions and direct hardware access. Key additions include concepts for constrained generics, ranges for composable algorithms, coroutines for async programming, modules for faster compilation, and std::expected for value-or-error returns.

Core Concepts

Concepts and Constraints

Concepts replace SFINAE with readable, composable type constraints.

#include <concepts>
#include <type_traits>

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<typename T>
concept Serializable = requires(T t, std::ostream& os) {
    { t.serialize(os) } -> std::same_as<void>;
    { T::deserialize(std::declval<std::istream&>()) } -> std::same_as<T>;
};

// Constrained function template
template<Numeric T>
T clamp(T value, T lo, T hi) {
    return value < lo ? lo : (value > hi ? hi : value);
}

// Abbreviated function template with auto
void process(Serializable auto& obj) {
    obj.serialize(std::cout);
}

Ranges and Views

Ranges enable lazy, composable pipelines over sequences.

#include <ranges>
#include <vector>
#include <algorithm>
#include <string>

namespace rv = std::ranges::views;

std::vector<int> data = {1, 5, 3, 8, 2, 9, 4, 7, 6};

// Lazy pipeline: filter, transform, take
auto result = data
    | rv::filter([](int n) { return n % 2 == 0; })
    | rv::transform([](int n) { return n * n; })
    | rv::take(3);
// Evaluates lazily: 64, 4, 16

// Chunk and slide views (C++23)
for (auto chunk : data | rv::chunk(3)) {
    // Process groups of 3
}

// Zip multiple ranges (C++23)
std::vector<std::string> names = {"alice", "bob", "charlie"};
std::vector<int> scores = {95, 87, 92};

for (auto [name, score] : rv::zip(names, scores)) {
    std::println("{}: {}", name, score);
}

Coroutines

C++20 coroutines support generators and async patterns.

#include <coroutine>
#include <optional>

template<typename T>
struct Generator {
    struct promise_type {
        T current_value;

        Generator get_return_object() {
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T value) {
            current_value = std::move(value);
            return {};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;

    bool next() {
        handle.resume();
        return !handle.done();
    }

    T value() const { return handle.promise().current_value; }

    ~Generator() { if (handle) handle.destroy(); }
};

Generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        auto tmp = a + b;
        a = b;
        b = tmp;
    }
}

Modules

Modules replace header files with faster, more encapsulated compilation units.

// math.cppm - module interface unit
export module math;

export namespace math {
    template<std::integral T>
    constexpr T gcd(T a, T b) {
        while (b != 0) {
            auto t = b;
            b = a % b;
            a = t;
        }
        return a;
    }

    consteval double pi() { return 3.14159265358979323846; }
}

// main.cpp - consumer
import math;

int main() {
    auto result = math::gcd(48, 18); // 6
}

std::expected (C++23)

std::expected provides Rust-like Result semantics for error handling without exceptions.

#include <expected>
#include <string>
#include <system_error>

enum class ParseError { InvalidFormat, OutOfRange, Empty };

std::expected<int, ParseError> parse_int(std::string_view sv) {
    if (sv.empty()) return std::unexpected(ParseError::Empty);

    int result = 0;
    for (char c : sv) {
        if (c < '0' || c > '9')
            return std::unexpected(ParseError::InvalidFormat);
        result = result * 10 + (c - '0');
    }
    return result;
}

// Monadic operations (and_then, transform, or_else)
auto doubled = parse_int("42")
    .transform([](int v) { return v * 2; })
    .or_else([](ParseError e) -> std::expected<int, ParseError> {
        return 0; // Default on error
    });

Implementation Patterns

RAII with Smart Pointers and Custom Deleters

#include <memory>

struct FileDeleter {
    void operator()(FILE* f) const {
        if (f) std::fclose(f);
    }
};

using FilePtr = std::unique_ptr<FILE, FileDeleter>;

FilePtr open_file(const char* path, const char* mode) {
    return FilePtr(std::fopen(path, mode));
}

// Shared ownership with weak references for caches
class TextureCache {
    std::unordered_map<std::string, std::weak_ptr<Texture>> cache_;
public:
    std::shared_ptr<Texture> load(const std::string& path) {
        if (auto it = cache_.find(path); it != cache_.end()) {
            if (auto tex = it->second.lock()) return tex;
        }
        auto tex = std::make_shared<Texture>(path);
        cache_[path] = tex;
        return tex;
    }
};

Compile-Time Programming with constexpr/consteval

consteval auto make_lookup_table() {
    std::array<int, 256> table{};
    for (int i = 0; i < 256; ++i) {
        table[i] = (i * i) % 256;
    }
    return table;
}

constexpr auto lookup = make_lookup_table(); // Computed at compile time

static_assert(lookup[16] == 0);

Structured Bindings and Pattern Matching

// Structured bindings with maps
std::unordered_map<std::string, int> scores = {{"alice", 95}, {"bob", 87}};

for (const auto& [name, score] : scores) {
    std::println("{} scored {}", name, score);
}

// std::variant with std::visit
using Value = std::variant<int, double, std::string>;

auto to_string = [](const Value& v) -> std::string {
    return std::visit([](const auto& val) -> std::string {
        if constexpr (std::is_same_v<std::decay_t<decltype(val)>, std::string>)
            return val;
        else
            return std::to_string(val);
    }, v);
};

Best Practices

  • Prefer std::expected or error codes over exceptions in performance-critical or embedded code paths.
  • Use concepts to constrain templates; they produce clearer error messages than SFINAE.
  • Prefer std::span<T> over raw pointer + size pairs for function parameters.
  • Use constexpr and consteval to move computation to compile time wherever possible.
  • Prefer std::string_view over const std::string& for non-owning string parameters.
  • Use std::move semantics and pass sink parameters by value to enable moves.
  • Adopt modules for new code to reduce compile times and improve encapsulation.

Common Pitfalls

  • Dangling references from std::string_view pointing to temporary strings that go out of scope.
  • Forgetting that std::ranges::views are lazy and invalidated if the underlying container mutates.
  • Coroutine lifetime bugs: the coroutine frame is heap-allocated and must outlive all references to its state.
  • Using std::shared_ptr where std::unique_ptr suffices adds unnecessary atomic reference counting overhead.
  • Misunderstanding constexpr vs consteval: constexpr permits runtime evaluation; consteval does not.
  • Module support varies across compilers; check your toolchain's support before relying on modules in production.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add systems-programming-skills

Get CLI access →