Cpp Modern
Modern C++ (C++20/23) patterns including concepts, ranges, coroutines, and modules
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 linesModern 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::expectedor 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
constexprandconstevalto move computation to compile time wherever possible. - Prefer
std::string_viewoverconst std::string&for non-owning string parameters. - Use
std::movesemantics 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_viewpointing to temporary strings that go out of scope. - Forgetting that
std::ranges::viewsare 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_ptrwherestd::unique_ptrsuffices adds unnecessary atomic reference counting overhead. - Misunderstanding
constexprvsconsteval:constexprpermits runtime evaluation;constevaldoes 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
Related Skills
Build Systems
Build systems for systems programming including CMake, Meson, and Zig build with cross-compilation
Ffi
Foreign function interfaces for calling between C, C++, Rust, Zig, Python, and other languages safely
Memory Safety
Techniques for achieving memory safety without garbage collection including ownership models, arenas, and static analysis
Zig Basics
Zig language fundamentals for systems programming including comptime, error handling, and manual memory management
Adversarial Code Review
Adversarial implementation review methodology that validates code completeness against requirements with fresh objectivity. Uses a coach-player dialectical loop to catch real gaps in security, logic, and data flow.
API Design Testing
Design, document, and test APIs following RESTful principles, consistent