C+
For LLMs

Writing correct C+: a guide for language models

This page is written for a model about to generate or edit C+. It is deliberately dense and imperative. Humans new to the language will prefer the builder track; this is the reference you load before you write.

Prime directive: the compiler is the source of truth. C+ is designed so that what compiles is correct by construction. Do not guess and stop. Generate, then run cpc check (single file) or cpc build (project), read the numbered diagnostic, and repair. Every rule below is compiler-enforced, with the error code you hit when you break it. The loop is short on purpose: write, check, repair.

Invocation modes

The thirteen rules, all compiler-enforced

# Rule Do this instead Error
1 No null Option[T]; FFI null is 0 as *T in unsafe E0300
2 No closures or lambdas named fn; stateful callbacks via (fn_ptr, user_data: *u8) E0100
3 No &T / &mut T types borrowing is a parameter marker E0100
4 No exceptions, try, throw, ? tagged-union values, match or guard let E0001
5 No implicit conversions every width change uses as E0302
6 No overloading one name, one signature E0301
7 No macros, decorators, comptime only compiler-known attributes E0354
8 No class / function / var struct + impl, fn, let E0100
9 No mutable-by-default mut is opt-in sema
10 Generics use [T], not <T> fn f[T](...), Vec[i32] E0100
11 Explicit return end every function body with return EXPR; E0333
12 :: for types, . for instances Type::assoc(), value.method() E0303 / E0327
13 Module-private by default pub is the export marker E0403

Syntax canon (shapes that compile)

// types: i8..i64 isize, u8..u64 usize, f32 f64, bool, (), str, string, *T, fn(..)->R
let x: i32 = 5;             // immutable
let mut z: i32 = 0; z = 7;  // mutable is opt-in
let n: u64 = 42u64;         // typed literal; 0x1F, 0b1010, 1_000_000, 'a' (u8)

// control flow — condition must be bool; no integer truthiness
if c { ... } else if d { ... } else { ... }
let r: i32 = if c { 1 } else { 2 };
while p { ... }
for i in 0..n { ... }       // 0..n exclusive, 0..=n inclusive
loop { if done { break; } continue; }

// structs + receivers
struct Point { x: i32, y: i32 }            // no field shorthand: write `x: x`
impl Point {
    fn new(x: i32, y: i32) -> Point { return Point { x: x, y: y }; }  // :: assoc fn
    fn read(self) -> i32 { return self.x; }          // reads, does not consume
    fn shift(mut self, d: i32) { self.x = self.x +% d; return; }  // mutates in place
    fn take(move self) -> i32 { return self.x; }     // consumes self
}
// receivers are self / mut self / move self ONLY. There is no `borrow self`.

// enums + matching — always spell type args at the source
enum Maybe[T] { Some(T), None }
let m: Maybe[i32] = Maybe[i32]::Some(7);
return match s {                            // exhaustive or E0340
    Shape::Circle(r)  => (r as i32),
    Shape::Rect(w, h) => (w as i32) *% (h as i32),
};
guard let Maybe[i32]::Some(v) = m else { return 0 -% 1; };  // else must diverge

// generics + turbofish
fn identity[T](x: T) -> T { return x; }
let v = vec::with_capacity::[i32](16 as usize);
let sz = #size_of::[Point]();

Gotchas that produce real errors:

Ownership: markers, not reference types

There is no &T and no &mut T. The default for a non-Copy value is move.

Parameter form Non-Copy type Copy type
x: T move (caller can't use x after) pass-by-value copy
borrow x: T shared borrow, caller keeps it (redundant)
mut x: T exclusive borrow, mutations propagate local mutable copy

A type that defines fn drop(mut self) is forced non-Copy. Most common borrow error is E0335 (use of moved value): you used a value after passing it by move. Fix preference order: add a { } scope so a borrow ends earlier; add borrow so the callee does not consume; .clone(); or restructure ownership. Return values always move. A str / T[] view may not outlive the value it points into (E0513 if it views a local).

Errors are values

enum Parse { Ok(i32), Bad, Overflow }
fn run(s: str) -> i32 {
    guard let Parse::Ok(v) = parse(s) else { return 0 -% 1; };
    return v +% 1;
}

No ?, no propagation operator, no exceptions. Match exhaustively or use guard let for the happy path.

FFI: both directions

// Call C: declare extern, call inside unsafe.
extern fn malloc(n: usize) -> *u8;
let p: *u8 = unsafe { malloc(64 as usize) };   // every *T op needs unsafe

// Be called by C: pub extern fn exports an unmangled C symbol.
pub extern fn cplus_add(a: i32, b: i32) -> i32 { return a +% b; }

cpc --emit-obj f.cplus -o f.o emits a standard relocatable object; cpc --emit-header f.cplus writes the matching C header. A C or C++ build links the object like any other. #[repr(C)] goes on structs that cross the boundary, not on functions. Pointer-to-int casts go through usize (E0315 otherwise). SIMD types do not cross an extern fn boundary; round-trip via [f32; N] (E0410).

Intrinsics are spelled #name(...)

#size_of::[T](), #align_of::[T](), #addr_of(place) (unsafe), #zero::[T](), #include_bytes("path"), #include_str("path"), #selector("sel"), #msg_send(recv, "sel", ...) -> T. The old bare-name and !-suffix forms are errors.

Never propose

null · &x / &mut x · a closure or lambda · try / catch / throw / ? · function overloading · implicit numeric conversion · class / function / var · <T> generics · an implicit (returnless) function body · Type.method() or value::method() (wrong separator) · borrow self · for v in array · struct field shorthand · string + concatenation.

Error-code quick reference

Code Meaning Fix
E0100 Parser: wrong form (closure, <T>, class, borrow self) use the C+ form
E0300 Undefined name (incl. null) typo / missing import / pub
E0301 Duplicate definition no overloading, rename
E0302 Type mismatch insert as or fix the type
E0312 for...in needs range or Iterator[T] index 0..n
E0315 Invalid cast pointer↔int via usize
E0327 Wrong call form Type::assoc() vs value.method()
E0333 Implicit return add return EXPR;
E0335 Use of moved value scope / borrow / .clone() / restructure
E0340 Non-exhaustive match add arm or _
E0345 Possibly-unassigned binding init on every path
E0801 Unsafe op outside unsafe wrap pointer ops / extern calls
E0900 Borrow-shaped param in async fn pass string / Vec[T]
E0901 / E0907 #[no_alloc] / #[no_block] violation remove the allocation / blocking call

The self-audit loop

  1. Generate the smallest compiling unit.
  2. cpc check FILE --diagnostics=json (single file) or cpc check (project).
  3. For each diagnostic: the code tells you the rule, the span tells you the exact site, the message usually tells you the fix.
  4. Repair and re-check. Do not narrate uncertainty to the user when the compiler will answer in one call.

When unsure about a construct, write the three-line program that exercises it and check it. The compiler is faster and more reliable than reasoning about whether it compiles.


‹ Back to all guides