Skip to content

Types and Nullability

Andrew Lind edited this page Sep 22, 2026 · 8 revisions
  • Float lowers to C++ double (64-bit, matching Haxe's Float on every official target — never float, which would silently halve the precision); genuine single-precision is available as Single or hxcpp's cpp.Float32 → C++ float (and cpp.Float64double).
  • A floating literal takes the f suffix in a cpp.Float32 context — an argument to a cpp.Float32 parameter, a local/field initialiser or assignment, a return, an Array<cpp.Float32> element, or a struct-literal field: SetPerspective(70.0f, 0.1f, 100.0f), float f = 0.1f;. A bare C++ floating literal is a double, so without the suffix every such line narrows at the conversion and MSVC reports C4305 — the value is identical either way, but a VC6 build is expected to compile clean. A genuine Float / double context is left alone, and so is arithmetic: Float arithmetic is double arithmetic whatever it is assigned to, so return a * 0.5; keeps its double operand and narrows once at the end (see the next bullet).
  • A narrowing conversion is an explicit cast. A value stored into a smaller scalar than it has — (uint16_t)(base + 1) for an Int expression pushed into an Array<cpp.UInt16>, (float)(a * 0.5) for a Float returned as cpp.Float32 — is cast rather than left to the implicit conversion, wherever the target type is known (a call argument, an initialiser or assignment, a return, a container element, a struct-literal field). The conversion happens either way, so nothing changes at runtime: the cast states the intent, keeps MSVC quiet (C4244), and avoids handing VC6 an implicit narrowing of a loop-derived value, which it has been seen miscompiling under /O2. Only genuine narrowings qualify — a smaller destination of the same kind, or a floating value into an integer; a literal is written in the target type already and is not cast. C++ integral promotion counts: n + 1 where n is a cpp.UInt16 is an int expression, so storing it back into a uint16_t is a narrowing.
  • Division semantics are preserved: / always yields Float, so two statically-known-integer operands divide as double (a / b((double)(a) / b); Std.int(a / b) truncates back, as in Haxe); % with a float operand lowers to fmod (C89 <math.h>, portable to VC6 — C++ % is integer-only). More broadly, arithmetic with a Float operand yields Float (+, -, *, /, %), so a var inferred from intField / floatField is a double, not a truncated int.
  • The full shift set including the unsigned >>> / >>>= (no C++ spelling — lowered through an unsigned int cast, (int)((unsigned int)a >> b)).
  • Null<T> and optional value-structs lower uniformly to T* (with matching heap-allocation at call sites); Map.get(k) lowers to an iterator with an existence check. A Null<T> over a value T (e.g. Null<String>) is an owned heap pointer: null is NULL, assigning a value heap-wraps (new T(v), freeing any prior value), a value-position read dereferences (NULL → a default T), != null is a real pointer check, and the destructor frees it.
  • A plain String is non-nullable — it lowers to a value std::string, which has no null. So s == null / s != null on a value String is a hard error (a value string is never null): test emptiness with s != "", or use Null<String> for a genuinely nullable string. The one exception is an optional ?s:String parameter, which defaults to "", so a "was it passed?" check legitimately reads as s.empty().
  • final constants lower to namespace-scoped static const (no #define), namespace-qualified across boundaries.
  • A typedef alias is transparent — it takes on the shape of whatever it names, resolved through before every pointer/reference/container/dispatch decision, while the alias name is kept in the emitted spelling. typedef Color = cpp.UInt32 is the primitive uint32_t (by value, an optional ?c:Color gets a default); typedef Panel = Widget is the reference class (Panel*, -> dispatch, methods/fields resolve through it); typedef Ints = Array<Int> is that container (const Ints& param — see Statements & Expressions); typedef Vertex = Pt is the value struct (const Vertex&, field access works); typedef Name = String keeps the const Name& optimization; and Null<Ptr> over a pointer alias stays a single pointer.
  • A member's type resolves where it is declared, not where it is used. The field types of a typedef struct or class — and a method's return type — are looked up in the scope of the module that declares them, so an unrelated same-named type the using module imports can never hijack them. With gfx.Mesh = { vertices:Array<Vertex> } meaning gfx::Vertex, a module that also imports a ui.Vertex proxy still indexes mesh.vertices[i].x as a value struct (., not ->), and a nested literal (line = { a: { x: 1.0, y: 2.0 }, … }) or array-of-struct literal ({ vertices: [ {…} ] }) is built as gfx::Vertex / std::vector<gfx::Vertex> — no explicitly typed local is needed. See Interop via @proxy for the common way this collision arises.
  • Pointer interop typescpp.RawPointer<T> / cpp.Star<T>T*, cpp.ConstStar<T>const T*, and cpp.Voidvoid (so cpp.RawPointer<cpp.Void> is void*). Dynamic / Any in an emitted position also erase to void*. These and the .raw idioms have their own page — see Raw-Pointer Interop.

Clone this wiki locally