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 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 struct + impl, fn; locals are let / var E0100
9 Mutability is explicit var (local), static (global), ref (write-back) E0305 / E0328
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 via _, public by default leading _ is private; export marks the C-ABI surface 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 (freezes value + fields)
var z: i32 = 0; z = 7;      // mutable local
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(this) -> i32 { return this.x; }          // reads, does not consume
    fn shift(ref this, d: i32) { this.x = this.x +% d; return; }  // mutates in place
    fn take(take this) -> i32 { return this.x; }     // consumes this
}
// receivers are this / ref this / take this ONLY. The name is always `this`.

// 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 every type (a bare x: T) is a read-only borrow — the caller keeps ownership.

Parameter form Meaning Caller
x: T read-only borrow (any type) keeps x; may read it after
ref x: T by-reference write-back; the callee mutates the caller's value place must be var
take x: T ownership transfer (move); the value is consumed can't use x after

A type that defines fn drop(ref this) is forced non-Copy. A bare non-Copy value can be read freely but may not escape the callee (be returned, stored in a field, or re-passed to a take slot) — that would create a second owner, so use take x: T instead (E0337). The other common conflict is E0335 (use of a take-moved value): you used a value after it was consumed. Fix preference order: add a { } scope so a borrow ends earlier; make the binding var (for ref); take / .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, then call it. The extern declaration is the marker.
extern fn malloc(n: usize) -> *u8;
let p: *u8 = malloc(64 as usize);   // *T ops are bare — the syntax is self-flagging

// Be called by C: export extern fn emits an unmangled C symbol.
export 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 use the loud #addr(p) intrinsic and go through usize (E0315 on a forbidden cast). 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) (place to *T), #addr(p) (the loud pointer-to-int form), #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 · mut / self / Self / move / borrow / unsafe / pub / trait (rejected keywords) · <T> generics · an implicit (returnless) function body · Type.method() or value::method() (wrong separator) · a receiver other than this / ref this / take this · for v in array · struct field shorthand · string + concatenation.

Error-code quick reference

Code Meaning Fix
E0100 Parser: wrong form (closure, <T>, class, a bad receiver) use the C+ form
E0300 Undefined name (incl. null) typo / missing import / drop a leading _
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()
E0328 ref arg / mutating method needs a var place declare the binding var
E0333 Implicit return add return EXPR;
E0335 Use of a take-moved value scope / take / .clone() / restructure
E0337 A bare borrow escapes (return / field-store / re-pass to take) take it by value (take) or .clone()
E0340 Non-exhaustive match add arm or _
E0345 Possibly-unassigned binding init on every path
E0403 Private (_-prefixed) symbol used across modules drop the leading _, or export it
E0900 Borrow-shaped param in async fn pass Text / 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