Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Language · C++23 · GCC 16 · Clang 22 · CMake 4.3

C++

RAII, value semantics and no raw new/delete — modern C++.

cppcpp20raii

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You 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++23 is 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++2c only 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) over printf/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. Target cmake_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 mix import 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-wide apt install of libs.
  • Third-party (only when stdlib lacks it): {fmt} 12.2.0 only if you must target a stdlib without std::print (12.0 added constexpr fmt::format); spdlog for structured logging; Abseil for flat_hash_map; Boost only for a specific, named component.
  • Tooling: clang-format 22, clang-tidy 22, ASan/UBSan/TSan, include-what-you-use optional. Same major version as your Clang. Export CMAKE_EXPORT_COMPILE_COMMANDS=ON so clang-tidy/IWYU/editors read a real compile_commands.json.

Project conventions

  • Layout: include/<project>/*.hpp for public headers, src/*.cpp for 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/variables snake_case, constexpr/macros SCREAMING_SNAKE (avoid macros entirely where possible), private members trailing _ (size_). Namespaces short and lowercase; wrap the whole library in one project namespace.
  • namespace: never using namespace std; in a header, ever; avoid it in .cpp too — prefer explicit std:: or a scoped using std::vector; inside a function.
  • Formatting: .clang-format at repo root (base LLVM or Google, 100-col, PointerAlignment: Left). CI runs clang-format --dry-run --Werror. No manual alignment.
  • .clang-tidy committed at root with WarningsAsErrors and 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, no malloc/free. Heap ownership is std::make_unique<T>(...); shared ownership (genuinely shared lifetime) is std::make_shared<T>(...). make_* gives exception safety and one allocation.
  • std::unique_ptr is the default owning pointer. Reach for std::shared_ptr only when ownership is truly shared and its end is not statically known; break cycles with std::weak_ptr. A shared_ptr passed where a reference would do is a code smell.
  • Custom C resources get a unique_ptr with a stateless deleter or a small RAII class:
    struct FileCloser { void operator()(std::FILE* f) const noexcept { if (f) std::fclose(f); } };
    using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
    
    For a C API that fills a T**/T*& out-parameter, adapt your smart pointer with std::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[]; use std::vector, std::array (fixed size), or std::string. Pass buffers as std::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." Never delete through 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::move into the member:
    explicit User(std::string name) : name_{std::move(name)} {}
    
  • std::string_view for read-only string params, std::span for 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::move a local in a return statement (it disables NRVO). Do not return const by value.
  • std::move is 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 noexcept so std::vector reallocation moves instead of copies.
  • Get comparisons from auto operator<=>(const T&) const = default; (with defaulted operator==) rather than hand-writing six operators; the compiler derives <, <=, >, >=, ==, != consistently.

const-correctness and constexpr

  • Mark member functions const whenever they don't mutate observable state. Mark locals and parameters const by default. Use std::as_const to force the const overload (e.g. to iterate a container read-only) rather than a const_cast.
  • Make functions and variables constexpr when their inputs can be compile-time; use consteval for functions that must run at compile time and constinit to 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 #define constants with constexpr/constinit; replace function-like macros with constexpr/consteval functions 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 with std::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 plain enum. Give it an explicit underlying type when it matters. Use std::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 -1 or nullptr-as-absent.
  • No C arrays, no char* strings, no malloc, no printf/scanf, no memcpy between non-trivially-copyable types, no strcpy/strcat/sprintf. Use std::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 or std::views::enumerate to avoid unsigned-underflow bugs.
  • Prefer std::from_chars/std::to_chars and std::format over atoi/stringstream for 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 throwaway std::vector or exposing an iterator pair by hand.

Error handling

  • Recoverable, expected failures: return std::expected<T, E> (C++23) where E is a rich error type or enum class error code; check it, propagate with .and_then/.transform/.or_else. Use std::optional<T> when the only failure is "absent" and the caller needs no reason.
  • Programming bugs / broken invariants: assert (debug) or a hard std::abort/std::terminate via 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 by const&, and keep functions exception-safe (strong or nothrow guarantee). Mark leaf/never-throwing functions noexcept.
  • Never ignore an error. No empty catch(...) {}, no discarding a returned expected/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/span to a local or to a temporary. Watch dangling from returned views of function-local std::strings.
  • Don't take the address to signal mutation — an T& out-param is clearer, though prefer returning a value or struct.
  • 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), not std::thread: it auto-joins on destruction and carries a std::stop_token for cooperative cancellation. Never leave a bare std::thread that you must remember to join().
  • Lock with RAII: std::scoped_lock for one or more mutexes (deadlock-free multi-lock), std::lock_guard for a single one, std::unique_lock only when you need to unlock early or use a condition variable. Never call mutex.lock()/unlock() by hand.
  • For read-heavy shared state, std::shared_mutex with std::shared_lock (readers) and std::unique_lock (writers) beats a plain mutex; measure before assuming the extra overhead pays off.
  • Shared state is either behind a mutex or is std::atomic<T> (std::atomic_ref to atomically access non-atomic storage). volatile is not for threading — it provides no atomicity or ordering.
  • Prefer std::atomic with the default seq_cst ordering; only weaken to acquire/release/relaxed with a written justification and a TSan-verified rationale.
  • Coordinate with std::latch, std::barrier, std::counting_semaphore, and condition_variable with a predicate (guard against spurious wakeups). For task graphs, prefer std::execution senders/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-function std::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, not reinterpret_cast punning), no reinterpret_cast between unrelated types, no invalid downcasts (use dynamic_cast or a checked variant), no nullptr deref.
  • 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 -fwrapv to 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_P cases 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 sleep for 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 with std::chrono by 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() or std::span/std::mdspan in 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_overflow builtins (GCC/Clang), C23 ckd_add/ckd_mul (<stdckdint.h>), C++26 std::add_sat/std::mul_sat (saturating, <numeric>), or compute in a wider type — never a plain a + b you assume didn't wrap.
  • Never system(), popen(), or shell-concatenate untrusted input; use posix_spawn/exec* with an argv array. Validate/canonicalize paths; open with O_NOFOLLOW where relevant.
  • Treat all external input as hostile: validate length and range before use, use std::from_chars with error checks, and prefer std::string_view parsing that never runs off the end.
  • No secrets in source or logs. Zero sensitive buffers after use (a volatile-barrier memset or std::fill that 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_ptr by default, shared_ptr only for shared lifetime.
  • Pass const& to read, by value + std::move for sinks, string_view/span for 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 -Werror on both GCC and Clang; run clang-tidy, clang-format, ASan/UBSan/TSan in CI.
  • Keep headers self-contained with #pragma once and include-what-you-use.

Avoid

  • new/delete/malloc/free, owning raw pointers, new[] — use make_unique/containers.
  • Manual copy/move/destructor outside a dedicated RAII wrapper (violates rule of zero).
  • using namespace std; in a header (banned) — qualify with std::.
  • Plain enum, C arrays, char* strings, printf/sprintf/strcpy, #define constants, C-style casts.
  • std::thread you must remember to join (use jthread), manual lock()/unlock() (use scoped_lock), volatile for threading (use std::atomic).
  • Returning references/span/string_view to locals; storing a view past its data's lifetime.
  • std::move in return of a local (kills NRVO); reading a moved-from object; discarding an expected/error.
  • Empty catch(...){}, sentinel error values (-1, nullptr-as-absent), throwing non-std::exception types.

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/conanfile with a pinned version and wire it via CMake find_package/target_link_libraries — never #include a copied-in source file.
  • Ask before: changing the public ABI/API of a shipped header, bumping the -std level, 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.

Back to top ↑