Modern Cpp
Trail of BitsOfficialPublished by Trail of Bits
- Skill
- Guidelines
- Software development
- CC-BY-SA-4.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a comprehensive framework for transitioning C++ codebases toward modern standards. It emphasizes patterns that eliminate common vulnerability classes, such as memory leaks and buffer overflows, by replacing legacy C-style constructs with modern, type-safe alternatives. The content includes migration tables for anti-patterns, compiler hardening configurations, and feature tiers based on practical usability.
What it does
- Provides migration paths from legacy C++ patterns to modern C++20/23/26 idioms.
- Lists specific anti-patterns and their safer, modern replacements.
- Details compiler hardening flags for GCC and Clang to improve security.
- Explains the use of hardened libc++ modes for bounds-checking.
- Categorizes C++ features by their current practical deployment readiness.
- Offers a checklist for adopting safe coding practices in new and existing projects.
When to use it
- When writing new C++ functions, classes, or libraries.
- When modernizing legacy C++ codebases to improve safety and maintainability.
- When working on security-critical or safety-sensitive C++ applications.
- When reviewing C++ code to ensure adoption of modern idioms.
- When configuring compiler and linker settings for enhanced security.
Advantages
- Reduces vulnerability classes like memory leaks and buffer overflows.
- Improves code readability and reduces boilerplate through modern language features.
- Provides actionable, security-focused compiler and linker configurations.
- Offers clear, evidence-based guidance on replacing legacy C-style constructs.
- Helps align code with current C++ standards for better long-term maintainability.
Usage examples
- “How should I replace raw pointers in my C++ code?”
- The skill recommends using std::unique_ptr for exclusive ownership or std::shared_ptr for shared ownership to ensure RAII semantics and eliminate manual memory management.
- “What are the recommended compiler flags for hardening a C++ project?”
- The skill provides a list of essential flags including -D_FORTIFY_SOURCE=3, -fstack-protector-strong, and -ftrivial-auto-var-init=zero, along with linker settings like -Wl,-z,relro,-z,now.
- “How can I replace printf in C++23?”
- The skill suggests using std::print or std::println, which are type-safe and avoid the format string vulnerabilities associated with printf.
- “What is the modern alternative to SFINAE?”
- The skill advises using C++20 Concepts and the requires clause to constrain templates, which provides more readable code and clearer error messages.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/trailofbits/modern-cpp/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip modern-cpp.zip -d .agents/skills
Documentation by Trail of Bits
Modern C++
Guide for writing modern C++ using C++20, C++23, and C++26 idioms. Focuses on patterns that eliminate vulnerability classes and reduce boilerplate, with a security emphasis from Trail of Bits.
When to Use This Skill
Writing new C++ functions, classes, or libraries
Modernizing existing C++ code (pre-C++20 patterns)
Choosing between legacy and modern approaches
Working on security-critical or safety-sensitive C++
Reviewing C++ code for modern idiom adoption
When NOT to Use This Skill
User explicitly requires older standard: Respect constraints (embedded, legacy ABI)
Pure C code: This skill is C++-specific
Build system questions: CMake, Meson, Bazel configuration is out of scope
Non-C++ projects: Mixed codebases where C++ isn't primary
Anti-Patterns to Avoid
| Avoid | Use Instead | Why |
|---|---|---|
new/delete | std::make_unique, std::make_shared | Eliminates leaks, double-free |
| Raw owning pointers | std::unique_ptr, std::shared_ptr | RAII ownership semantics |
C arrays (int arr[N]) | std::array<int, N> | Bounds-aware, value semantics |
| Pointer + length params | std::span<T> | Non-owning, bounds-checkable |
printf / sprintf | std::format, std::print | Type-safe, no buffer overflow |
C-style casts (int)x | static_cast<int>(x) | Explicit intent, auditable |
#define constants | constexpr variables | Scoped, typed, debuggable |
SFINAE / enable_if | Concepts + requires | Readable constraints and errors |
| Error codes + out params | std::expected<T, E> | Composable, type-safe errors |
union | std::variant | Type-safe, no silent UB |
Raw mutex.lock()/unlock() | std::scoped_lock | Exception-safe, no deadlocks |
std::thread | std::jthread | Auto-join, stop token support |
assert() macro | contract_assert (C++26) | Visible to tooling, configurable |
| Manual CRTP | Deducing this (C++23) | Simpler, no template boilerplate |
| Macro code generation | Reflection (C++26) | Zero-overhead, composable |
See anti-patterns.md for the full table (30+ patterns).
Decision Tree
What are you doing?
|
+-- Writing new C++ code?
| +-- Use modern idioms by default (C++20/23)
| +-- Choose the newest standard your compiler supports
| +-- See Feature Tiers below
|
+-- Modernizing existing code?
| +-- Start with Tier 1 (C++20/23) replacements
| +-- Prioritize by security impact (memory > types > style)
| +-- See anti-patterns.md for the migration table
|
+-- Security-critical code?
| +-- Enable compiler hardening flags (see below)
| +-- Enable hardened libc++ mode
| +-- Run sanitizers in CI
| +-- See safe-idioms.md and compiler-hardening.md
|
+-- Using C++26 features?
+-- Reflection: YES, plan for it (GCC 16+)
+-- Contracts: cautiously, for new API boundaries
+-- std::execution: wait for ecosystem maturity
+-- See cpp26-features.mdFeature Tiers
Features are ranked by practical usability today, not by standard version.
Tier 1: Use Today (C++20/23, solid compiler support)
| Feature | Replaces | Standard |
|---|---|---|
Concepts + requires | SFINAE, enable_if | C++20 |
| Ranges + views | Raw iterator loops | C++20 |
std::span<T> | Pointer + length | C++20 |
std::format | sprintf, iostream chains | C++20 |
Three-way comparison <=> | Manual comparison operators | C++20 |
std::jthread | std::thread + manual join | C++20 |
| Designated initializers | Positional struct init | C++20 |
std::expected<T,E> | Error codes, exceptions at boundaries | C++23 |
std::print / std::println | printf, std::cout << | C++23 |
Deducing this | CRTP, const/non-const duplication | C++23 |
std::flat_map | std::map for read-heavy use | C++23 |
Monadic std::optional | Nested if-checks on optionals | C++23 |
See cpp20-features.md and cpp23-features.md.
Tier 2: Deploy Now (no standard bump needed)
These improve safety without changing your C++ standard version:
Compiler hardening flags —
-D_FORTIFY_SOURCE=3,-fstack-protector-strong,-ftrivial-auto-var-init=zeroHardened libc++ —
-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FASTfor ~0.3% overhead bounds-checkingSanitizers in CI — ASan + UBSan as minimum; TSan for concurrent code
Warning flags —
-Wall -Wextra -Wpedantic -Werror
Tier 3: Plan For (C++26, worth restructuring around)
Reflection is the single most transformative C++26 feature. It eliminates:
Serialization boilerplate (one generic function replaces per-struct
to_json)Code generators (protobuf codegen, Qt MOC)
Macro-based registration and enum-to-string hacks
GCC 16 (April 2026) has reflection merged. Plan new code to benefit from it.
Tier 4: Watch (C++26, needs maturation)
Contracts (
pre/post/contract_assert) — Better thanassert(), but no virtual function support and limited compiler support. Adopt cautiously for new API boundaries.std::execution (senders/receivers) — Powerful async framework, but steep learning curve, no scheduler ships with it, and poor documentation. Wait for ecosystem maturity.
See cpp26-features.md.
Compiler Hardening Quick Reference
Essential Flags (GCC + Clang)
-Wall -Wextra -Wpedantic -Werror
-D_FORTIFY_SOURCE=3
-fstack-protector-strong
-fstack-clash-protection
-ftrivial-auto-var-init=zero
-fPIE -pie
-Wl,-z,relro,-z,nowClang-Specific
-Wunsafe-buffer-usageHardened libc++ (Clang/libc++ only)
-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FASTGoogle deployed this across Chrome and their server fleet: ~0.3% overhead, 1000+ bugs found, 30% reduction in production segfaults.
See compiler-hardening.md for the full guide.
Rationalizations to Reject
| Rationalization | Why It's Wrong |
|---|---|
| "It compiles without warnings" | Warnings depend on which flags you enable. Add -Wall -Wextra -Wpedantic. |
| "ASan is too slow for production" | Use GWP-ASan for sampling-based production detection (~0% overhead). |
| "We only use safe containers" | Iterator invalidation and unchecked optional access are still exploitable. |
| "Smart pointers are slower" | std::unique_ptr has zero overhead vs raw pointers. Measure before claiming. |
| "Our code doesn't have memory bugs" | Google found 1000+ bugs when enabling hardened libc++. So did everyone else. |
| "C++26 features aren't available yet" | C++20/23 features are. Hardening flags work on any standard. Start there. |
| "Modern C++ is harder to read" | std::expected is more readable than checking error codes across 5 out-params. |
Best Practices Checklist
[ ] Use smart pointers for ownership, raw pointers only for non-owning observation
[ ] Prefer
std::spanover pointer + length for function parameters[ ] Use
std::expectedfor functions that can fail with typed errors[ ] Constrain templates with concepts, not SFINAE
[ ] Enable compiler hardening flags and hardened libc++ in all builds
[ ] Run ASan + UBSan in CI; add TSan for concurrent code
[ ] Use
constexpr/constevalwhere possible (UB-free by design)[ ] Mark functions
[[nodiscard]]when ignoring the return value is likely a bug[ ] Prefer value semantics; use
std::variantoverunion,enum classoverenum[ ] Initialize all variables at declaration
Read Next
anti-patterns.md — Full legacy-to-modern migration table (30+ patterns)
cpp20-features.md — Concepts, ranges, span, format, coroutines
cpp23-features.md — expected, print, deducing this, flat_map
cpp26-features.md — Reflection, contracts, memory safety improvements
compiler-hardening.md — Flags, sanitizers, hardened libc++
safe-idioms.md — Security patterns by vulnerability class
Files
- ATTRIBUTION.mdAdded by AutoGineer924 B
- LICENSELicence file added by AutoGineer19.7 KiB
- SKILL.md8.5 KiB
- references/anti-patterns.md5.5 KiB
- references/compiler-hardening.md7.3 KiB
- references/cpp20-features.md7.0 KiB
- references/cpp23-features.md7.0 KiB
- references/cpp26-features.md7.5 KiB
- references/safe-idioms.md8.1 KiB
references/cpp20-features.md
C++20 Features (Default Practice)
These features are mature, well-supported (GCC 12+, Clang 14+, MSVC 17.0+), and should be the default way to write C++.
Concepts
Constrain templates with readable, declarative requirements.
What They Replace
SFINAE and std::enable_if — the arcane template tricks that produced unreadable error messages.
Standard Concepts
Use concepts from <concepts> before writing custom ones:
#include <concepts>
template <std::integral T>
T gcd(T a, T b) { return b == 0 ? a : gcd(b, a % b); }
template <std::invocable<int> F>
void apply(F&& fn, int value) { fn(value); }Key standard concepts: std::integral, std::floating_point, std::invocable, std::copyable, std::movable, std::regular, std::predicate, std::ranges::range.
Custom Concepts
template <typename T>
concept Serializable = requires(T obj, std::ostream& os) {
{ obj.serialize(os) } -> std::same_as<void>;
{ T::deserialize(os) } -> std::same_as<T>;
};
void save(const Serializable auto& obj, std::ostream& os) {
obj.serialize(os);
}requires Clauses vs concept Keyword
concept: reusable, named constraint — define when used in 2+ placesrequires: inline, one-off constraint
// One-off: use requires
template <typename T>
requires (sizeof(T) <= 16)
void small_copy(T value);
// Reusable: define a concept
template <typename T>
concept SmallType = sizeof(T) <= 16;Before / After
// BEFORE: SFINAE — cryptic, fragile
template <typename T,
typename = std::enable_if_t<std::is_arithmetic_v<T>>>
T square(T x) { return x * x; }
// Error: "no matching function for call to 'square'" + 50 lines of template noise
// AFTER: concepts — readable, clear errors
template <typename T>
requires std::is_arithmetic_v<T>
T square(T x) { return x * x; }
// Error: "constraint 'is_arithmetic_v<string>' not satisfied"Ranges
Composable, lazy algorithms that operate on range objects instead of iterator pairs.
Key Patterns
#include <ranges>
#include <algorithm>
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// View pipeline — lazy, no intermediate allocations
auto result = data
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
// Range algorithms — accept ranges directly
std::ranges::sort(data);
auto it = std::ranges::find(data, 42);
auto [mn, mx] = std::ranges::minmax(data);To materialize a lazy view into a concrete container, use std::ranges::to<>() from C++23 — see cpp23-features.md.
Useful Views
| View | Purpose |
|---|---|
views::filter | Keep elements matching predicate |
views::transform | Apply function to each element |
views::take(n) | First n elements |
views::drop(n) | Skip first n elements |
views::split(delim) | Split range on delimiter |
views::join | Flatten nested ranges |
views::enumerate | Pairs of (index, element) — C++23 |
views::zip | Zip multiple ranges — C++23 |
views::chunk(n) | Group into chunks of n — C++23 |
views::concat | Concatenate ranges — C++26 |
std::span<T>
Non-owning view over contiguous memory. Replaces pointer + length pairs.
#include <span>
// BEFORE: dangerous — no size information
void process(int* data, size_t len);
// AFTER: carries size, bounds-checkable
void process(std::span<int> data) {
for (auto& val : data) { /* safe iteration */ }
data[0]; // unchecked by default; bounds-checked under hardened libc++
// (_LIBCPP_HARDENING_MODE). span has no .at() until C++26.
}
// Works with any contiguous container
std::vector<int> vec = {1, 2, 3};
std::array<int, 3> arr = {4, 5, 6};
int c_arr[] = {7, 8, 9};
process(vec); // implicit conversion
process(arr); // implicit conversion
process(c_arr); // implicit conversionUse std::span<const T> for read-only views.
std::format
Type-safe string formatting, replacing sprintf and iostream chains.
#include <format>
std::string s = std::format("Hello, {}!", name);
std::string t = std::format("{:>10.2f}", 3.14159); // right-aligned, 2 decimal
std::string u = std::format("{:#x}", 255); // 0xff
std::string v = std::format("{:%Y-%m-%d}", std::chrono::system_clock::now());Custom Formatters
template <>
struct std::formatter<Point> {
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
auto format(const Point& p, format_context& ctx) const {
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
// Now works:
std::println("Position: {}", my_point);Three-Way Comparison (<=>)
Generate all comparison operators from one declaration.
#include <compare>
struct Point {
int x, y;
auto operator<=>(const Point&) const = default;
// Generates: ==, !=, <, >, <=, >=
};For custom ordering:
struct Version {
int major, minor, patch;
std::strong_ordering operator<=>(const Version& other) const {
if (auto cmp = major <=> other.major; cmp != 0) return cmp;
if (auto cmp = minor <=> other.minor; cmp != 0) return cmp;
return patch <=> other.patch;
}
bool operator==(const Version&) const = default;
};Designated Initializers
Named field initialization for aggregates.
struct Config {
int port = 8080;
bool verbose = false;
int max_connections = 100;
};
// Clear which fields are being set
Config cfg = {.port = 3000, .verbose = true};
// .max_connections gets its default value (100)std::jthread
Thread with automatic joining and cooperative cancellation.
#include <thread>
// BEFORE: forgetting to join causes std::terminate
std::thread t(work);
// ... if exception thrown here, t.join() never called → crash
// AFTER: auto-joins on destruction
std::jthread t(work);
// destructor calls request_stop() then join() — always safe
// Cooperative cancellation via stop token
std::jthread worker([](std::stop_token stoken) {
while (!stoken.stop_requested()) {
do_work();
}
});
worker.request_stop(); // signal the thread to exit
// destructor waits for thread to finishCoroutines
Suspendable functions for lazy computation and async I/O.
When to Use
I/O-bound async work (network, file system)
Lazy sequence generation (use
std::generatorfrom C++23)State machines that would otherwise be complex
When NOT to Use
CPU-bound parallel work (coroutines cooperate, they don't parallelize)
Simple synchronous code (unnecessary complexity)
Reimplementing coroutine types (
generator,task) that libraries already provide
Custom awaiters are fine when adapting async libraries to coroutines — that's often the only way to bridge callback-based APIs.
With std::generator (C++23)
std::generator<int> range(int start, int end) {
for (int i = start; i < end; ++i)
co_yield i;
}
for (auto val : range(0, 10)) {
std::println("{}", val);
}Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Trail of Bits
- Publisher
- Trail of Bits (Official)
- Source
- trailofbits/skills · plugins/modern-cpp/skills/modern-cpp · commit e4dd0bb
- Licence
- Creative Commons Attribution-ShareAlike 4.0 International (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Trail of Bits.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download modern-cpp.zip