DEV Community

Olivia Craft
Olivia Craft

Posted on

CLAUDE.md for C++: 13 Rules That Make AI Write Safe, Modern, Idiomatic C++

CLAUDE.md for C++: 13 Rules That Make AI Write Safe, Modern, Idiomatic C++

C++ is the language where AI assistance can either save you thousands of hours — or introduce bugs that hide in production for months. The difference comes down to whether you've told your AI assistant which C++ you're writing.

Without a CLAUDE.md, the model might write C++98 in a C++20 codebase, reach for raw pointers when you expect smart pointers, or generate undefined behavior that passes every test until the wrong CPU architecture exposes it.

These 13 rules have emerged from real projects — the patterns that matter most for keeping AI-generated C++ safe, modern, and idiomatic.


Rule 1: Pin the standard — C++20 or C++23 only

Standard: C++20 minimum. Use C++23 features where available.
Enable: concepts, ranges, coroutines, std::span, std::format.
Forbidden: C++98/11/14 idioms when modern equivalents exist.
Enter fullscreen mode Exit fullscreen mode

AI models have seen enormous amounts of legacy C++ code. Without this rule, you'll get a mix of std::bind and lambdas, printf next to std::format, and iterators where ranges would be cleaner.

Why it matters: C++20 concepts alone eliminate entire classes of template error messages and make intent explicit. std::span replaces the pointer+size anti-pattern. The standard pin prevents the model from defaulting to the lowest common denominator.

// Without rule — AI might generate this
template<typename T>
void process(T* data, size_t n) { ... }

// With rule — AI generates this
template<std::ranges::range R>
void process(R&& range) { ... }
Enter fullscreen mode Exit fullscreen mode

Rule 2: Smart pointers only — raw owning pointers banned

Memory: std::unique_ptr / std::shared_ptr for ownership.
Raw pointers: observer pointers only (never owning).
Forbidden: new/delete in application code. malloc/free never.
Use std::make_unique and std::make_shared exclusively.
Enter fullscreen mode Exit fullscreen mode

The most common AI-generated C++ mistake is mixing ownership models. A function that accepts T* when it means "I own this" is a memory leak waiting to happen — and AI generates this pattern constantly without explicit instruction.

// Banned — raw owning pointer
Widget* createWidget() {
    return new Widget();  // Who deletes this?
}

// Required — ownership explicit
std::unique_ptr<Widget> createWidget() {
    return std::make_unique<Widget>();
}
Enter fullscreen mode Exit fullscreen mode

The observer pointer exception: T* and T& are fine as non-owning references — they communicate "I borrow this, I don't own it."


Rule 3: RAII for every resource — no manual cleanup

Resources: wrap every resource in a RAII type.
File handles  std::fstream. Locks  std::lock_guard / std::unique_lock.
Network/OS handles  custom RAII wrapper with destructor.
No resource acquired without a destructor that releases it.
Enter fullscreen mode Exit fullscreen mode

AI will generate fclose(file) at the end of a function. It won't always generate it on every early return. RAII removes the entire problem.

// AI without rule — leak on early return
FILE* f = fopen("data.bin", "rb");
if (!validate()) return;  // leak
process(f);
fclose(f);

// With rule — RAII, always closed
{
    std::ifstream f("data.bin", std::ios::binary);
    if (!validate()) return;  // destructor runs
    process(f);
}
Enter fullscreen mode Exit fullscreen mode

Rule 4: No undefined behavior — ever

UB rules:
- No signed integer overflow. Use checked arithmetic or unsigned where wrap is expected.
- No out-of-bounds access. Use .at() in non-hot paths. Assert bounds in hot paths.
- No uninitialized reads. Initialize all variables at declaration.
- No dangling references. No returning references to locals.
- No strict aliasing violations. Use std::bit_cast instead of reinterpret_cast.
Enter fullscreen mode Exit fullscreen mode

This is the rule AI needs most explicitly. UB that "works" on x86 can crash on ARM. UB that passes tests can be exploited by compilers doing aggressive optimization. Models will generate UB because it compiles.

// UB — uninitialized variable
int result;
if (condition) result = compute();
return result;  // undefined if !condition

// Required — always initialized
int result = 0;
if (condition) result = compute();
return result;
Enter fullscreen mode Exit fullscreen mode

Rule 5: Prefer value semantics — move is free

Value semantics: pass by value when moving is cheap.
Return by value  NRVO/RVO makes this free.
std::move explicitly only when transferring ownership.
Const references for large objects you only read.
Avoid output parameters  return structs or std::tuple instead.
Enter fullscreen mode Exit fullscreen mode

AI defaults to C-style output parameters and reference-heavy signatures. Modern C++ with move semantics makes value semantics fast and clean.

// AI without rule — output parameter style
void getResult(std::vector<int>& out) {
    out = compute();
}

// With rule — value semantics
std::vector<int> getResult() {
    return compute();  // NRVO, zero copy
}
Enter fullscreen mode Exit fullscreen mode

Rule 6: Concepts for every template constraint

Templates: constrain with concepts, never unconstrained.
Use standard concepts: std::integral, std::floating_point, std::ranges::range, std::invocable.
Define project concepts in a concepts.hpp header.
No typename T without a constraint.
Enter fullscreen mode Exit fullscreen mode

Unconstrained templates produce error messages that span 50 lines. Concepts produce one clear diagnostic at the call site.

// Without rule — unconstrained template
template<typename T, typename Func>
auto transform(T container, Func f) { ... }

// With rule — concepts constrain intent
template<std::ranges::input_range R, std::invocable<std::ranges::range_value_t<R>> F>
auto transform(R&& range, F&& f) { ... }
Enter fullscreen mode Exit fullscreen mode

Rule 7: Error handling with std::expected or exceptions — no error codes

Error handling:
- Functions that can fail: return std::expected<T, Error> (C++23) or throw typed exceptions.
- No int return codes. No errno. No output parameters for errors.
- Exception types: derive from std::runtime_error or std::logic_error.
- Error type: define a project Error enum or variant.
- No catch(...) without rethrow or logging.
Enter fullscreen mode Exit fullscreen mode

AI generates error codes because of the enormous volume of C legacy code in training data. C++ deserves typed error handling.

// Banned — C-style error code
int readFile(const std::string& path, std::string& out);

// Required — typed result
std::expected<std::string, FileError> readFile(const std::filesystem::path& path);
Enter fullscreen mode Exit fullscreen mode

Rule 8: const everywhere it can be

const rules:
- All variables that don't change: const.
- All member functions that don't mutate: const.
- All parameters you don't modify: const ref.
- Prefer constexpr for compile-time constants over const.
- consteval for functions that must compute at compile time.
Enter fullscreen mode Exit fullscreen mode

const communicates intent, enables optimizations, and prevents a class of bugs. AI forgets it unless you insist.

// Without rule — missed const opportunities
std::string getName(User user) {
    std::string result = user.name;
    return result;
}

// With rule — const-correct
std::string getName(const User& user) {
    const std::string result = user.name;
    return result;
}
Enter fullscreen mode Exit fullscreen mode

Rule 9: std::span and std::string_view — not raw arrays or const char*

Buffers: std::span<T> for contiguous ranges. Never T* + size.
Strings: std::string_view for read-only string parameters. Never const char*.
Ownership strings: std::string. Never char[].
No C string functions: strlen, strcpy, sprintf banned.
Enter fullscreen mode Exit fullscreen mode

std::span and std::string_view are zero-cost abstractions that express intent precisely. const char* parameters can't communicate lifetime or bounds.

// Banned — raw C-style
void process(const char* data, size_t len);

// Required — expressive and safe
void process(std::span<const std::byte> data);
void log(std::string_view message);
Enter fullscreen mode Exit fullscreen mode

Rule 10: Structured bindings and pattern matching — use them

Structured bindings: use for tuple/pair/struct decomposition.
std::visit with overloaded lambdas for std::variant.
if constexpr for compile-time branching in templates.
Avoid index-based tuple access (std::get<0>).
Enter fullscreen mode Exit fullscreen mode

This makes AI-generated C++ readable. Index-based access and manual pair.first/pair.second are noise.

// Without rule — index access
auto result = getCoordinates();
double x = std::get<0>(result);
double y = std::get<1>(result);

// With rule — structured binding
auto [x, y] = getCoordinates();
Enter fullscreen mode Exit fullscreen mode

Rule 11: Thread safety is explicit — no implicit sharing

Concurrency:
- Shared mutable state: protected by std::mutex always.
- Prefer std::atomic for single-value shared state.
- No global mutable state. No static mutable locals in multithreaded code.
- std::jthread over std::thread (automatic join).
- std::latch / std::barrier / std::counting_semaphore over manual condition variables.
Enter fullscreen mode Exit fullscreen mode

AI generates code with races because races don't crash on the first test run. Making thread safety explicit in the config catches it before review.

// Without rule — silent data race
static int counter = 0;
void increment() { ++counter; }

// With rule — explicit atomic
static std::atomic<int> counter{0};
void increment() { counter.fetch_add(1, std::memory_order_relaxed); }
Enter fullscreen mode Exit fullscreen mode

Rule 12: Build with warnings-as-errors — the compiler is a reviewer

Build flags (required):
-Wall -Wextra -Wpedantic -Werror
-Wconversion -Wshadow -Wnull-dereference
Sanitizers in debug: -fsanitize=address,undefined
clang-tidy: enabled with modernize-*, cppcoreguidelines-* checks
Enter fullscreen mode Exit fullscreen mode

AI-generated code that compiles is not finished. Warnings-as-errors forces the model to generate cleaner code when you describe the constraint explicitly in CLAUDE.md.


Rule 13: Modules or namespaces — never pollute global scope

Namespaces: every symbol in a project namespace. No using namespace std in headers.
Modules (C++20): prefer over #include where toolchain supports.
Anonymous namespaces: for translation-unit-local symbols.
No #define for constants  use constexpr.
No using namespace X in headers  ever.
Enter fullscreen mode Exit fullscreen mode

AI loves using namespace std; because it appears in every tutorial. It's banned in production headers.

// Banned in headers
using namespace std;

// Required
namespace myproject {
    constexpr std::size_t kMaxBufferSize = 4096;

    class DataProcessor {
        std::vector<std::byte> buffer_;
    public:
        std::expected<void, ProcessError> process(std::span<const std::byte> input);
    };
}
Enter fullscreen mode Exit fullscreen mode

The CLAUDE.md block for C++

## C++ Standards

**Standard:** C++20 minimum (C++23 where available)
**Compiler:** Clang 16+ or GCC 13+
**Build:** -Wall -Wextra -Wpedantic -Werror -Wconversion -Wshadow

### Memory
- std::unique_ptr / std::shared_ptr for ownership. std::make_unique / std::make_shared only.
- Raw pointers = non-owning observer only. new/delete banned in application code.
- RAII for all resources — file handles, locks, OS handles.

### Types and APIs  
- std::span<T> for buffers (never T* + size).
- std::string_view for read-only strings (never const char*).
- std::expected<T, E> for fallible operations (C++23). Typed exceptions otherwise.
- No C string functions: strlen/strcpy/sprintf banned.

### Templates
- Constrain every template parameter with concepts. No bare typename T.
- Structured bindings over index access. if constexpr over SFINAE.

### Safety
- No UB: initialize all variables. Use .at() outside hot paths. No signed overflow.
- const everywhere possible. constexpr for compile-time constants.
- No using namespace in headers. Every symbol in project namespace.

### Concurrency
- std::mutex for shared mutable state. std::atomic for single values.
- std::jthread over std::thread. No global mutable state.
Enter fullscreen mode Exit fullscreen mode

Why this matters

The 13 rules above don't describe a style preference. They describe the line between C++ that a senior engineer would ship and C++ that would be flagged in review — or worse, that ships and fails under sanitizers.

AI models have internalized decades of C++ code, most of it pre-C++11. Without explicit constraints, they'll default to the mean of that training distribution: raw pointers, error codes, unconstrained templates, and platform-specific assumptions.

The CLAUDE.md block above shifts that default. It doesn't eliminate review — but it raises the floor so that AI-generated C++ starts from a position worth reviewing.

The cost of writing this config once is about 15 minutes. The cost of reviewing AI-generated C++ without it is every sprint.

Top comments (0)