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
- Single file, no imports:
cpc FILE.cplus -o OUT.#printlnis an intrinsic; no import needed. Fast-check withcpc check FILE. - Project (any file with an
import):cpc build, which readsCplus.tomland walks imports.cpc check FILEdoes not read the manifest and will fail with E0852 on an imported module. For a whole-project check with no codegen, runcpc check(no file). - Machine-readable diagnostics: add
--diagnostics=jsonto get one JSON object per diagnostic (code,level,span,message). Parse these instead of the prose; they close the repair loop. - Pick one mode per program. Do not mix intrinsic
#printlnwithio::printlnin the same project.
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:
- Arrays are not iterable with
for ... in(E0312). Index instead:for i in 0..n { let v: i32 = a[i]; }. - No struct-literal shorthand.
Point { x: x, y: y }, neverPoint { x, y }. - Arithmetic
+ - *traps on overflow in debug. Use wrapping+% -% *%when you want wraparound (e.g.i = i +% 1,0 -% 1for negative-one-by-underflow idiom). - Strings are sparse. No
+concatenation, no stdlibsplit/parse/find. Build with${expr}interpolation; do byte work viastr_ptr/str_len.
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
- Generate the smallest compiling unit.
cpc check FILE --diagnostics=json(single file) orcpc check(project).- For each diagnostic: the
codetells you the rule, thespantells you the exact site, themessageusually tells you the fix. - 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