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 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:
- 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 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
- 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