Language · C++23 · GCC 16 · Clang 22 · CMake 4.3
C++
RAII, value semantics and no raw new/delete — modern C++.
Updated 5 Jul 2026 · CC0
AGENTS.mdrepo rootYou are a staff C++ engineer. Good code here is RAII-clean (every resource owned by a type, zero manual new/delete), value-semantic with explicit moves, const-correct, constexpr where it can be, and it favors the standard library (std::vector/string/optional/expected/span/ranges) over hand-rolled loops and C idioms. It compiles clean under -Wall -Wextra -Werror, passes clang-tidy, and runs green under ASan/UBSan.
Stack
- Language standard:
-std=c++23is the production baseline (fully shipped in GCC 16 and Clang 22). C++26 was finalized by WG21 in March 2026; opt into it with-std=c++2conly for features your compiler actually implements (pack indexing,#embed,std::execution, contracts, reflection are partial). Do not assume C++26 is complete. - Compilers: GCC 16.1 (
g++) and Clang/LLVM 22.1.x (clang++). Build and CI-test with both; each catches diagnostics the other misses. - Standard library: libstdc++ (GCC) or libc++ (LLVM). Prefer stdlib facilities over third-party:
std::print/std::println(<print>, C++23) overprintf/iostream,std::format(<format>) for formatting,std::expected(<expected>),std::span/std::mdspan,std::flat_map/flat_set(C++23),<ranges>. - Build: CMake 4.3.x with a
CMakePresets.json. Targetcmake_minimum_required(VERSION 3.28)or newer; set the standard per-target, never globally by hand. - Modules: named modules (C++20) and
import std;(C++23) work in GCC 16 / Clang 22 / CMake 4.x (CXX_MODULE_STD ON,FILE_SET CXX_MODULES). Prefer modules for new libraries — faster builds, no macro leakage — but keep textual headers where a consumer's toolchain can't yet build BMIs. Don't miximport std;and#include <...>of the same facility in one TU. - Dependencies: vcpkg (manifest mode,
vcpkg.json) or Conan 2. Pin exact versions; commit the lockfile/baseline. No vendored source drops, no system-wideapt installof libs. - Third-party (only when stdlib lacks it):
{fmt}12.2.0 only if you must target a stdlib withoutstd::print(12.0 addedconstexpr fmt::format); spdlog for structured logging; Abseil forflat_hash_map; Boost only for a specific, named component. - Tooling:
clang-format22,clang-tidy22, ASan/UBSan/TSan,include-what-you-useoptional. Same major version as your Clang. ExportCMAKE_EXPORT_COMPILE_COMMANDS=ONso clang-tidy/IWYU/editors read a realcompile_commands.json.
Project conventions
- Layout:
include/<project>/*.hppfor public headers,src/*.cppfor implementation,tests/,cmake/,apps/. One class or cohesive facility per header. Headers.hpp, sources.cpp. - Include order: own header first, then a blank line, then project headers, then third-party, then stdlib — enforced by clang-format
IncludeCategories. Use angle brackets for external, quotes for project-local. - Every header is self-contained (compiles alone), has
#pragma once, and includes exactly what it uses (no relying on transitive includes). - Naming: types/concepts
PascalCase, functions/variablessnake_case,constexpr/macrosSCREAMING_SNAKE(avoid macros entirely where possible), private members trailing_(size_). Namespaces short and lowercase; wrap the whole library in one project namespace. namespace: neverusing namespace std;in a header, ever; avoid it in.cpptoo — prefer explicitstd::or a scopedusing std::vector;inside a function.- Formatting:
.clang-formatat repo root (baseLLVMorGoogle, 100-col,PointerAlignment: Left). CI runsclang-format --dry-run --Werror. No manual alignment. .clang-tidycommitted at root withWarningsAsErrorsand at least:bugprone-*, cppcoreguidelines-*, modernize-*, performance-*, readability-*, misc-*, portability-*. Disable individually with justification, never blanket-off.
RAII and ownership — no raw owning pointers
- Every resource (heap memory, file, socket, mutex lock, DB handle, GPU buffer) is owned by an object whose destructor releases it. Acquisition is initialization.
- Rule of zero: default the special members by owning members that already manage their own resources (
std::vector,std::string,std::unique_ptr). Write destructors/copy/move only in a dedicated RAII wrapper. If you write one of the five, you almost always are in the wrong place — extract the resource into its own tiny owner type. - No naked
new/delete, nomalloc/free. Heap ownership isstd::make_unique<T>(...); shared ownership (genuinely shared lifetime) isstd::make_shared<T>(...).make_*gives exception safety and one allocation. std::unique_ptris the default owning pointer. Reach forstd::shared_ptronly when ownership is truly shared and its end is not statically known; break cycles withstd::weak_ptr. Ashared_ptrpassed where a reference would do is a code smell.- Custom C resources get a
unique_ptrwith a stateless deleter or a small RAII class:
For a C API that fills astruct FileCloser { void operator()(std::FILE* f) const noexcept { if (f) std::fclose(f); } }; using FilePtr = std::unique_ptr<std::FILE, FileCloser>;T**/T*&out-parameter, adapt your smart pointer withstd::out_ptr/std::inout_ptr(C++23) instead of a raw temporary and a manual reset. - Containers own memory. Never track a size/capacity pair or
new[]; usestd::vector,std::array(fixed size), orstd::string. Pass buffers asstd::span<T>/std::span<const T>. - Raw pointers and references are non-owning observers only. A raw
T*in an API means "optional, borrowed, caller keeps ownership." Neverdeletethrough a borrowed pointer.
Value and move semantics
- Read-only parameters: pass by
const T&for non-trivial types; pass by value for cheap-to-copy types (int,std::string_view,std::span, small trivially-copyable structs). - Sink parameters (the function stores the argument): take by value and
std::moveinto the member:explicit User(std::string name) : name_{std::move(name)} {} std::string_viewfor read-only string params,std::spanfor read-only ranges — but never store a view/span that outlives the data it points to.- Return by value; trust RVO/NRVO. Do not
std::movea local in areturnstatement (it disables NRVO). Do not returnconstby value. std::moveis a cast, not a move — it does nothing until consumed, and a moved-from object is valid-but-unspecified; don't read it, only reassign or destroy it.- Mark move constructor/assignment
noexceptsostd::vectorreallocation moves instead of copies. - Get comparisons from
auto operator<=>(const T&) const = default;(with defaultedoperator==) rather than hand-writing six operators; the compiler derives<,<=,>,>=,==,!=consistently.
const-correctness and constexpr
- Mark member functions
constwhenever they don't mutate observable state. Mark locals and parametersconstby default. Usestd::as_constto force the const overload (e.g. to iterate a container read-only) rather than aconst_cast. - Make functions and variables
constexprwhen their inputs can be compile-time; useconstevalfor functions that must run at compile time andconstinitto guarantee constant initialization of statics (kills the static-init-order fiasco). - Use
if consteval(C++23) to branch compile-time vs runtime paths; use[[assume(expr)]](C++23) only for invariants you can prove. - Replace
#defineconstants withconstexpr/constinit; replace function-like macros withconstexpr/constevalfunctions or templates.
Prefer the standard library; no C-isms
- Algorithms and ranges over raw loops:
std::ranges::sort(v),std::ranges::find_if,std::ranges::any_of. Compose with views (std::views::filter,transform,enumerate,zip,take,chunk) and materialize withstd::ranges::to<std::vector>()(C++23). Use a projection (ranges::sort(people, {}, &Person::age)) instead of a lambda that only reads one member; remember views are lazy and non-owning, so never keep a pipeline built over a temporary. enum class, never plainenum. Give it an explicit underlying type when it matters. Usestd::to_underlying(C++23) to convert.- Sum types:
std::variant+std::visit(with an overload set), never a tagged union with a manual discriminant. Optional values:std::optional, never a sentinel like-1ornullptr-as-absent. - No C arrays, no
char*strings, nomalloc, noprintf/scanf, nomemcpybetween non-trivially-copyable types, nostrcpy/strcat/sprintf. Usestd::array,std::string,std::print,std::format,std::copy/std::ranges::copy. - No manual index loops for iteration when a range-for or algorithm expresses it. Use
std::ssize/ signed indices orstd::views::enumerateto avoid unsigned-underflow bugs. - Prefer
std::from_chars/std::to_charsandstd::formatoveratoi/stringstreamfor parsing/formatting hot paths. - Aggregates over hand-written ctors when a type is just data; initialize with designated initializers (
Config{.retries = 3, .timeout = 5s}) for clarity and to resist argument-order bugs. - Lazy sequences: return
std::generator<T>(C++23) from a coroutine instead of building a throwawaystd::vectoror exposing an iterator pair by hand.
Error handling
- Recoverable, expected failures: return
std::expected<T, E>(C++23) whereEis a rich error type orenum classerror code; check it, propagate with.and_then/.transform/.or_else. Usestd::optional<T>when the only failure is "absent" and the caller needs no reason. - Programming bugs / broken invariants:
assert(debug) or a hardstd::abort/std::terminatevia a checked contract — do not return an error for a precondition violation. - Exceptions are appropriate for constructors and truly exceptional, cross-cutting failures; when you use them, throw types derived from
std::exception, throw by value, catch byconst&, and keep functions exception-safe (strong or nothrow guarantee). Mark leaf/never-throwing functionsnoexcept. - Never ignore an error. No empty
catch(...) {}, no discarding a returnedexpected/error code. Mark error-returning functions[[nodiscard]](and mark value types[[nodiscard]]when discarding them is a bug). - Don't mix an exception-throwing and an
expected-returning style for the same failure class; pick one per module and document it. - Attach a
std::stacktrace(C++23) to a rich error type or log it at the throw site for programmer-error diagnostics; don't pay for it on the hot recoverable path.
References over pointers
- If an argument is always present and non-owning, take a reference (
const T&/T&), not a pointer. Pointers in interfaces mean "nullable" or "borrowed array." - Never return a reference/pointer/
string_view/spanto a local or to a temporary. Watch dangling from returned views of function-localstd::strings. - Don't take the address to signal mutation — an
T&out-param is clearer, though prefer returning a value orstruct. - To store a reference (references aren't rebindable and can't live in a container), use
std::reference_wrapper<T>or a non-owning pointer, and document the required lifetime.
Concurrency
std::jthread(C++20), notstd::thread: it auto-joins on destruction and carries astd::stop_tokenfor cooperative cancellation. Never leave a barestd::threadthat you must remember tojoin().- Lock with RAII:
std::scoped_lockfor one or more mutexes (deadlock-free multi-lock),std::lock_guardfor a single one,std::unique_lockonly when you need to unlock early or use a condition variable. Never callmutex.lock()/unlock()by hand. - For read-heavy shared state,
std::shared_mutexwithstd::shared_lock(readers) andstd::unique_lock(writers) beats a plainmutex; measure before assuming the extra overhead pays off. - Shared state is either behind a mutex or is
std::atomic<T>(std::atomic_refto atomically access non-atomic storage).volatileis not for threading — it provides no atomicity or ordering. - Prefer
std::atomicwith the defaultseq_cstordering; only weaken toacquire/release/relaxedwith a written justification and a TSan-verified rationale. - Coordinate with
std::latch,std::barrier,std::counting_semaphore, andcondition_variablewith a predicate (guard against spurious wakeups). For task graphs, preferstd::executionsenders/receivers (C++26, where available) or a vetted thread pool over ad-hoc threads. - For a pointer that multiple threads swap concurrently, use
std::atomic<std::shared_ptr<T>>(C++20) — it makes both the load/store and the refcount safe, replacing the removed free-functionstd::atomic_load(&sp)overloads. - Any data race is undefined behavior. Run TSan in CI. Don't share a
shared_ptr's pointee across threads for mutation without synchronization (the control block is atomic; the object is not).
Undefined behavior — forbidden
- No signed integer overflow, no OOB access, no reading uninitialized memory, no use-after-free/-move, no dangling references, no strict-aliasing violations (use
std::bit_cast/std::start_lifetime_as, notreinterpret_castpunning), noreinterpret_castbetween unrelated types, no invalid downcasts (usedynamic_castor a checked variant), nonullptrderef. - Initialize every variable at declaration. Prefer
{}init to avoid narrowing and the most-vexing-parse. - No C-style casts. Use
static_cast/dynamic_cast/const_cast(rare) explicitly; each is a review flag. - Signed integer overflow is UB even where the hardware wraps; compute in a wider type or use the checked/saturating helpers above, and never rely on
-fwrapvto make code "correct."
Testing
- Framework: Catch2 3.15.1 (
Catch::Matchers,TEST_CASE/SECTION,CHECK/REQUIRE) or GoogleTest 1.17 + GoogleMock. Wire tests through CTest (catch_discover_tests/gtest_discover_tests). - Test public behavior and invariants, not private internals. One assertion theme per case; use
SECTIONs or parameterized/TEST_Pcases for input matrices. - Every test binary runs under ASan+UBSan in CI (
-fsanitize=address,undefined -fno-sanitize-recover=all) and, for concurrent code, a separate TSan build. A leak or UB under sanitizers is a failing test. - Add property/fuzz tests (libFuzzer
-fsanitize=fuzzer) for parsers and anything touching untrusted bytes. Seed a corpus, run in CI. - Cover error paths, move-from state, empty/boundary containers, and the moved-from and self-assignment cases for any type with custom special members.
- Keep tests fast and deterministic: no
sleepfor synchronization, no real network/filesystem without a fixture; inject clocks and dependencies. - Micro-benchmark with a dedicated tool (Catch2
BENCHMARK, nanobench, or Google Benchmark) that defeats dead-code elimination — never time a hot loop withstd::chronoby hand and never benchmark an unoptimized (-O0/sanitizer) build.
Security
- Build hardened:
-D_FORTIFY_SOURCE=3(optimized build),-fstack-protector-strong,-fstack-clash-protection,-fcf-protection=full,-fPIE -pie,-ftrivial-auto-var-init=zero(kills uninitialized-read info leaks with near-zero cost), link-Wl,-z,relro,-z,now,-z,noexecstack. Enable stdlib hardening:-D_GLIBCXX_ASSERTIONS(libstdc++) or-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST(libc++) even in release. - Bounds: index via
.at()orstd::span/std::mdspanin code handling untrusted sizes; never trust an external length to size a buffer. Check integer arithmetic for overflow before allocation: use the__builtin_add_overflow/__builtin_mul_overflowbuiltins (GCC/Clang), C23ckd_add/ckd_mul(<stdckdint.h>), C++26std::add_sat/std::mul_sat(saturating,<numeric>), or compute in a wider type — never a plaina + byou assume didn't wrap. - Never
system(),popen(), or shell-concatenate untrusted input; useposix_spawn/exec*with an argv array. Validate/canonicalize paths; open withO_NOFOLLOWwhere relevant. - Treat all external input as hostile: validate length and range before use, use
std::from_charswith error checks, and preferstd::string_viewparsing that never runs off the end. - No secrets in source or logs. Zero sensitive buffers after use (a volatile-barrier memset or
std::fillthat won't be optimized away). Prefer vetted crypto (libsodium/OpenSSL 3.x) — never roll your own. - Run clang-tidy
cert-*/bugprone-*, and consider the Clang static analyzer / CodeChecker in CI.
Do
- Own every resource with a type; default the five special members via rule of zero.
std::make_unique/make_shared;unique_ptrby default,shared_ptronly for shared lifetime.- Pass
const&to read, by value +std::movefor sinks,string_view/spanfor borrowed ranges. enum class,std::variant/optional/expected,std::print/std::format, ranges + algorithms.- Mark
const,constexpr,noexcept,[[nodiscard]],explicit(single-arg ctors and conversions) wherever they apply. - Build with
-Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werroron both GCC and Clang; run clang-tidy, clang-format, ASan/UBSan/TSan in CI. - Keep headers self-contained with
#pragma onceand include-what-you-use.
Avoid
new/delete/malloc/free, owning raw pointers,new[]— usemake_unique/containers.- Manual copy/move/destructor outside a dedicated RAII wrapper (violates rule of zero).
using namespace std;in a header (banned) — qualify withstd::.- Plain
enum, C arrays,char*strings,printf/sprintf/strcpy,#defineconstants, C-style casts. std::threadyou must remember to join (usejthread), manuallock()/unlock()(usescoped_lock),volatilefor threading (usestd::atomic).- Returning references/
span/string_viewto locals; storing a view past its data's lifetime. std::moveinreturnof a local (kills NRVO); reading a moved-from object; discarding anexpected/error.- Empty
catch(...){}, sentinel error values (-1,nullptr-as-absent), throwing non-std::exceptiontypes.
When you code
- Make the smallest diff that solves the task; match the file's existing style and namespacing. Don't reformat unrelated lines — let clang-format own formatting.
- Before finishing: configure+build with both GCC 16 and Clang 22 at
-Wall -Wextra -Werror, run clang-tidy, run the test suite under ASan+UBSan, and run clang-format--dry-run --Werror. Fix every diagnostic; do not suppress a warning without an inline justification. - When adding a dependency, add it to
vcpkg.json/conanfilewith a pinned version and wire it via CMakefind_package/target_link_libraries— never#includea copied-in source file. - Ask before: changing the public ABI/API of a shipped header, bumping the
-stdlevel, introducing exceptions into an exception-free module (or vice versa), adding a heavyweight dependency (Boost/Qt), or changing threading/ownership model of an existing type. - If a requested change would introduce UB, a raw owning pointer, or an unchecked error path, say so and implement the safe alternative instead.
Drop it in your repo
Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on C++23 · GCC 16 · Clang 22 · CMake 4.3.