Skip to content

Members and Access

Andrew Lind edited this page Jul 20, 2026 · 5 revisions

A Haxe private member maps to C++ protected, not private: Haxe private is accessible from subclasses (and Haxe has no "private even from subclasses" concept), so emitting C++ private would reject an inherited-member access that Haxe accepts. Hatchet therefore never emits C++ private — hidden members are protected (still closed to outside code, open to subclasses, matching Haxe).

Property accessors

Every pair of default/null/never (pure access control) is lowered to direct field access, the backing field hidden as protected behind a generated GetX for (default,null).

A generated getter over a container (Array/Mapstd::vector/std::map) or a value-struct field returns a mutable reference (T&), not a const T copy, so Haxe's reference-type mutation through the getter works: obj.items[k] = v; and in-place struct mutation compile and take effect. (A scalar getter still returns a const T value.)

Custom accessors with real Haxe routing — a user-written get_x/set_x is emitted as a real method and every access routes through it, external and internal alike, except inside the property's own accessors (Haxe's recursion rule): reads become get_x(), writes become set_x(v) — including constructor writes, and compound writes/++/--, which desugar exactly as Haxe does (x += vset_x(read + v), with a side-effecting receiver hoisted so it evaluates once); an accessor whose signature omits its return type gets the property's type, as Haxe infers it (so function set_x(x:Float) { return this.x = x; } is a double-returning function, not a value return from void).

Per Haxe physicality, a non-@:isVar (get,never) emits no backing field at all. A set property without a set_x keeps the Hatchet dialect: an auto-generated trivial SetX (with the value-vs-pointer const rule) and direct internal writes.

For owned pointer fields behind a custom setter, the setter's direct store is the single delete-before-overwrite site (routed callers never also free), and a setter that returns the field reads to the escape analysis as the value being handed out — the field then leans borrowed (leak over double-free, the standard bias; @owned opts the destructor in). See Memory Ownership.

(get, default) and dynamic access remain flagged as unsupported (see Diagnostics).

Static fields

A static field is class-scoped, not a per-instance member — it is never a this-> field. How it is emitted depends on its type and initializer:

  • Scalar / String with a literal (or no) initializer — a plain class static: static T NAME; in the header, with an out-of-line T Class::NAME = <literal>; definition. Reads are Class::NAME.
  • Anything else — a Meyers singleton. This covers any struct / container / reference-typed static (C++98 cannot constant-initialise one as a class-scope data member) and a scalar with a non-literal initializer (a call, new, arithmetic, …). The field becomes a static T& NAME() accessor whose function-local static holds the value, initialised on the first call. A final field returns const T& (immutable); a var stays writable through the reference. Reads are Class::NAME().

The Meyers form defers the initializer to first use instead of running it at an unspecified point in the C++ static-initialisation order (the static init order fiasco), which matters because such a static runs real code — e.g. static var Identity:Matrix = FillMatrix([...]). A function-local static is initialised exactly once by the language, so no guard flag is needed; when the initializer builds a temporary (such as the array here) that setup is folded into a one-off _init_* helper so it too runs once — the same idiom as a file-scoped Array final's builder. Reads resolve the same way whether written bare inside the class or qualified as Class.NAME from another class.

Because the accessor-vs-data-member choice is driven by the field's type, a consuming extern / @proxy binding — which carries no initializer — reads the field the same way the producing class emits it. A struct-typed static bound through an extern is called (native::Class::NAME()), matching the native Meyers accessor; a scalar static final stays a plain data-member read.

Other member metadata & forms

  • @libexport — export a class from the shared library via the portable <PREFIX>_CLASS visibility macro (was @:decl before v0.2.7; see Metadata).
  • @:overload(...) — a call is resolved to the matching C++ overload by argument type, else a hard error.
  • @cexportextern "C" export of a module-level function via a portable macro (was @:abi before v0.2.7).
  • abstract class and abstract function — an abstract method becomes a pure virtual virtual T f() = 0;, declared and never defined.
  • The base-from-member Holder idiom for constructors whose super(...) is not the first statement.

Clone this wiki locally