Features

How each language does it

A near-exhaustive matrix. Toggle languages to focus; each cell explains how the feature works there.

Columns: CC++HolyCZigHareOdinForth

Language Features

FeatureCC++HolyCZigHareOdinForth
Macros / PreprocessorA textual pass that rewrites source before compilation (defines, includes, conditionals). YesThe C preprocessor (cpp) is a separate textual pass: #include, #define object- and function-like macros, #if/#ifdef, #pragma, and the #/## stringize and token-paste operators. Powerful but unhygienic - macros are pure text substitution with no type or scope awareness. YesInherits the full C preprocessor (#include, #define, #if). Modern C++ deliberately steers away from it - constexpr, templates, and (since C++20) modules replace most macro uses - but the preprocessor is still part of the language. YesHolyC keeps a C-style preprocessor with #define, #include, #ifdef, and #if. It is largely C-compatible but pared down to fit the TempleOS JIT; there is no large external cpp stage since compilation happens just-in-time in the shell. NoZig has no preprocessor and no macros by design ("if it isn't written, it doesn't happen"). Compile-time code generation, conditional compilation (if (builtin.os...)), and constants are all done with ordinary comptime Zig and const/@import instead of text substitution. NoHare has no preprocessor and no macro system. Conditional compilation is handled at the build level with per-platform source files and the +tags mechanism, not with #ifdef. Constants are real def/const bindings. NoOdin has no C-style preprocessor or text macros. It uses compile-time constants (X :: 10), when blocks for conditional compilation, and #-prefixed compiler directives (e.g. #load, #config) that are real language constructs rather than textual substitution. YesForth has no #define preprocessor, but IMMEDIATE words and the CREATE ... DOES> defining-word mechanism let you run code during compilation and extend the compiler itself - a metaprogramming facility more powerful than text macros. [ ... ] switches to interpret state mid-definition.
Templates / GenericsWriting code parameterized over types, instantiated per concrete type at compile time. PartialNo real generics. Generic code is faked with void* plus casts (type-unsafe) or with X-macros / #define-based template tricks. C11 added _Generic for type-directed expression selection, but there are no parameterized types. YesTemplates are C++'s flagship generic mechanism: function and class templates are monomorphized per type at compile time with full inlining (zero-overhead). C++20 concepts constrain template parameters, and the STL (containers, algorithms) is built entirely on templates. NoHolyC is closer to C than C++ and has no templates or generics. You write concrete types or fall back on raw pointers (U8*) and casts, much like plain C. YesGenerics fall out of comptime: types are first-class compile-time values, so a generic is just a function taking a comptime T: type and returning a type/value. std.ArrayList(T) is a function call. No separate template language - it's all ordinary Zig run at compile time. NoHare deliberately omits generics to keep its specification small. Code that must be type-generic uses tagged unions, slices of *opaque/*void-style pointers, or per-type duplication. A few built-ins (len, append, alloc) are generic, but user code is not. YesOdin has parametric polymorphism: procedures and structs take type parameters prefixed with $ (e.g. $T: typeid), resolved and specialized at compile time. where clauses add constraints. Built-in dynamic arrays, slices, and maps are generic over their element type. PartialForth is typeless: every word already operates on raw cells, so a single definition works on "any" data without parameterization. There is no static generic type system - genericity is a consequence of having no types at all, not a language feature.
Comptime MetaprogrammingRunning the language itself at compile time to generate code, types, and constants. NoNo general compile-time execution. The preprocessor does text substitution and _Static_assert/constant expressions are evaluated, but you cannot run arbitrary C at compile time to generate code or types. Yesconstexpr/consteval functions run real C++ at compile time; template metaprogramming computes types and values; C++20 adds constexpr containers and if constexpr. Together they let large amounts of work - and code generation - happen before runtime. NoHolyC is JIT-compiled but has no comptime-style metaprogramming construct. Top-level statements execute at load/compile time (the compiler is the shell), but there is no facility to programmatically generate types or specialize code at compile time. Yescomptime is Zig's central feature: any expression can be forced to evaluate at compile time, types are comptime values, and comptime parameters/blocks generate code, fold constants, and build types - replacing macros, templates, and the preprocessor with one mechanism. NoHare has no compile-time code execution or metaprogramming. Constant expressions are folded, but you cannot run Hare at compile time to generate types or code - a deliberate simplicity choice. PartialOdin evaluates constant expressions and has when (compile-time if), #config/#load, and parametric polymorphism resolved at compile time, plus type reflection (reflect, typeid). It does not, however, run arbitrary user procedures at compile time the way Zig's comptime does. YesForth's IMMEDIATE words execute while compiling, and [ ... ] drops into the interpreter mid-definition to compute literals - so the full language runs at compile time. CREATE ... DOES> defines new defining words, letting programs extend the compiler itself.
Error Handling ModelHow a routine signals and a caller responds to failure. PartialNo language-level error handling. Conventions only: return an int/sentinel/NULL and set the global errno, or pass out-parameters. There are no exceptions; setjmp/longjmp exist but are rarely used for errors. Cleanup is manual, often via goto cleanup;. YesC++ has true exceptions (throw/try/catch) with stack unwinding that runs RAII destructors, giving exception-safe cleanup. Modern code also favors value-based errors: std::optional, std::expected (C++23), and error codes. noexcept marks non-throwing functions. PartialHolyC has a try/catch and a throw built around TempleOS exception handling (e.g. throw('FileErr') with a numeric/char-const code), used by the OS itself. It is coarser than C++ exceptions and there are no RAII destructors to run during unwinding; otherwise error signaling is C-style return codes. YesErrors are values via error unions (!T) and error sets - no exceptions and no hidden control flow. try propagates, catch handles, and errdefer schedules cleanup that runs only on the error path. Every failure is visible at the call site. YesErrors are ordinary values built on tagged unions: a function returns (T | error) and the caller uses match, the ! operator to assert success (abort on error), or ? to propagate. No exceptions; defer handles cleanup along every path. YesNo exceptions - errors are multiple return values (idiomatically an enum/union error returned alongside the result). or_return propagates an error up, or_else supplies a default, and or_continue/or_break work in loops. Error handling is plain, explicit control flow. PartialThe standard provides an exception word set: THROW raises an integer code that unwinds to the nearest CATCH, which restores the stacks and returns the code (0 = success). It works but is low-level and integer-coded; most Forth code still checks return flags by hand.
DeferScheduling cleanup to run automatically when the enclosing scope exits. NoNo defer. Cleanup is manual: pair every malloc with a free on every exit path, commonly centralized with a single goto cleanup; label at the end of a function. (GCC's __attribute__((cleanup)) is a non-portable extension.) Via libraryC++ has no defer keyword; the idiomatic equivalent is RAII - a destructor runs deterministically at scope exit, so resources free themselves. A general defer is achievable with a scope-guard helper (e.g. std::unique_ptr with a custom deleter, or a ScopeExit/gsl::finally type). NoHolyC has no defer and no RAII destructors. Memory and resources are released manually with Free() on each exit path, just as in C. Yesdefer schedules a statement to run at scope exit in reverse order; errdefer runs only when the scope is left via an error. This keeps alloc/free adjacent without destructors - the standard cleanup idiom in Zig. Yesdefer runs a statement when the enclosing scope exits (in reverse order of registration), the canonical way to release memory and handles since Hare has no destructors. Yesdefer runs a statement (or block) at end of scope in reverse order. With no RAII, it is the standard cleanup tool - defer free(p), defer delete(arr), defer os.close(fd) - pairing acquisition and release line-by-line. NoForth has no scoping construct and therefore no defer. Cleanup is explicit: you pair ALLOCATE with FREE and open/close by hand, often wrapping risky code in CATCH to ensure resources are released on the error path.
Operator OverloadingGiving built-in operators (+, ==, []) custom meaning for user-defined types. NoOperators are fixed to built-in scalar/pointer types and cannot be overloaded. Operations on structs are done with named functions (e.g. vec_add(a, b)). YesNearly every operator (+, ==, [], (), <<, *, ->, conversions) can be overloaded as a member or free function. C++20 adds the <=> "spaceship" operator for auto-generated comparisons. Used pervasively (iterators, smart pointers, streams). NoHolyC does not support operator overloading; it kept C's fixed operators and dropped most of C++'s machinery. Use ordinary functions for user types. NoNo operator overloading by design - arithmetic operators work only on numeric types, so there is no hidden behavior behind +. Custom types use named methods (a.add(b)), keeping operator semantics obvious. NoHare has no operator overloading; operators apply only to built-in types. Operations on user types are expressed as ordinary function calls, in keeping with the language's minimalism. NoOdin intentionally omits operator overloading (a stated design decision to avoid hidden cost and ambiguity). Built-in math operators do work element-wise on Odin's built-in fixed array/vector types, but user structs use named procedures. PartialThere are no operators to overload - +, *, = are just words. You are free to redefine any of them or define typed variants (F+, D+, your own V+), but this is plain word redefinition, not type-directed overloading.
Namespaces / ModulesGrouping declarations to control naming and visibility across a codebase. PartialNo namespaces or modules. Scope is controlled per translation unit: static gives internal linkage (file-private) and headers expose what's shared. Name clashes are avoided by manual prefixing (png_read, sqlite3_open). Yesnamespace groups names and can be nested; using brings names into scope; anonymous namespaces give internal linkage. C++20 adds first-class modules (export module foo;) as a faster, isolation-friendly alternative to header inclusion. NoHolyC has no namespaces or modules. TempleOS uses one flat global symbol table across the whole running system, so names are global and clashes are avoided by convention. YesEvery Zig file is implicitly a struct, and @import("foo.zig") returns it as a namespace value bound to a const. Declarations are private unless marked pub; container types (structs/enums) also act as namespaces. No separate module keyword needed. YesHare has a real module system: a directory of .ha files is a module, imported with use path::to::mod; and referenced as mod::symbol. Identifiers are module-private unless marked export. YesA package is a directory of .odin files sharing a namespace; import "core:fmt" (or a relative path) makes it available as fmt.println. Top-level names are package-private by default; @(private)/export-style visibility attributes refine this. PartialClassic Forth has a single global dictionary (one flat namespace). The standard adds an optional search-order / wordlist word set (WORDLIST, VOCABULARY, SET-CURRENT, ALSO/PREVIOUS) that provides namespace-like scoping, but it is optional and dialect-dependent.
First-Class Function PointersTreating functions as values you can store, pass, and call indirectly. YesFunction pointers are first-class values: store them in variables/structs, pass them as callbacks, and build dispatch tables (qsort's comparator, vtables). No closures - they capture nothing beyond the bare code address. YesHas C function pointers plus richer callables: pointer-to-member, std::function (type-erased), and lambdas that capture state and can carry closures - far beyond a raw code address. YesHolyC supports C-style function pointers and uses them throughout TempleOS for callbacks and dispatch. They are plain code addresses, like C's. YesFunctions are first-class: *const fn(...) ... pointer types can be stored and passed for runtime dispatch, and functions/types are also usable as comptime values. No closures over a stack environment. YesHare has first-class function pointer types (*fn(x: int) int) that can be stored, passed, and called indirectly for callbacks and dispatch tables. No capturing closures. YesProcedures are first-class values: a procedure variable has a proc type, can be stored in structs/arrays, and called indirectly. Used for callbacks and vtable-style dispatch. Plain procedure values, not capturing closures. Yes' (tick) and ['] push a word's execution token (xt) onto the stack, and EXECUTE calls it - so words are first-class callable values. Deferred words (DEFER/IS) and xt tables in arrays give indirect dispatch.
Inline AssemblyEmbedding raw CPU instructions directly inside source code. PartialNot in the ISO standard, but every major compiler supports it as an extension: GCC/Clang's asm/__asm__ with input/output operand and clobber constraints, MSVC's __asm. Ubiquitous in OS and embedded code, just non-portable. PartialSame story as C: the standard reserves the asm declaration but leaves it implementation-defined; in practice you use the compiler's extended asm/__asm__ with operand constraints. Common in low-level code, non-portable. YesInline assembly is first-class in HolyC: an asm { ... } block (and the reg/#exe machinery) lets you write x86-64 directly, with the JIT assembling it. TempleOS, being ring-0 and self-hosted, leans on this heavily. YesFirst-class asm volatile (...) with explicit output/input operand and clobber lists tied to Zig values - fully part of the language, used for syscalls and intrinsics. (Available for supported architectures.) NoHare has no inline-assembly construct in the language. The standard library's low-level pieces (e.g. syscall stubs in rt) are written in separate .s assembly files and linked in, rather than embedded inline. YesOdin supports inline assembly via the built-in asm(...) expression: you provide the instruction string, a procedure-type signature, and constraints, and it lowers through LLVM. Used for intrinsics and syscalls. PartialNot in the core standard, but virtually every native Forth ships an assembler word set: CODE ... END-CODE defines a word's body in assembly using Forth's own RPN assembler syntax. Available everywhere in practice, but the syntax is implementation-specific.
Named / Default / Variadic ArgumentsCalling conveniences: argument names at call sites, default values, and variable arity. PartialVariadic functions exist via ... and <stdarg.h> (va_list/va_arg) - e.g. printf - but they are type-unsafe. There are no named arguments and no default arguments; designated initializers on a struct passed by value are the usual workaround. PartialDefault arguments are supported (void f(int x, int y = 0)), and variadics come in two forms: C-style ... and type-safe variadic templates (parameter packs). There are no named (keyword) arguments - designated initializers (C++20) on an options struct are the idiom. YesHolyC notably allows default arguments in any position (not just trailing), supports calling with omitted args, and has its own variadic mechanism (every function can read the implicit argc/argv of its extra ... arguments, as Print does). A standout convenience over plain C; no keyword-named args. PartialNo default args and no Zig-level named args - every parameter is positional and required (an anonymous struct literal is the idiom for optional/named fields). Variadics aren't a Zig feature, but comptime-generated functions and @call cover the cases; C-style varargs are only for C interop (extern). PartialHare has native variadic functions - a const... typed parameter pack iterated with vastart/vaarg (fmt::printf takes const... fmt::field); separately, C-ABI varargs are reachable via valist for C interop. It has no default arguments and no named arguments - parameters are positional, matching its minimalist design. YesOdin supports all three: default values (proc(x: int, y := 0)), named arguments at the call site (f(y = 3, x = 1)), and variadic parameters (args: ..int, plus ..any for fmt). A genuinely ergonomic call syntax. NoForth words take no declared parameters at all - every argument is passed implicitly on the data stack. There is no notion of named, default, or variadic parameters; a word simply consumes however many cells it documents in its ( -- ) stack comment.

Memory Management

FeatureCC++HolyCZigHareOdinForth
Manual heap allocation & freeExplicitly request and release heap memory; you own every byte's lifetime. YesThe canonical model: malloc(n) / calloc(n, sz) return void*, and you must call free(p) exactly once. No ownership tracking, no GC - leaks and double-frees are on you. YesInherits C malloc/free, but idiomatic C++ uses new/delete (which call constructors/destructors) and new[]/delete[] for arrays. Raw new/delete is discouraged in favor of smart pointers. YesTempleOS HolyC provides MAlloc(size) and Free(ptr) as the heap primitives (note the capitalized names). Allocation is tied to the current task's heap unless a heap pointer is passed. YesNo global malloc; you call methods on an explicit Allocator. allocator.alloc(T, n) / allocator.free(slice) and allocator.create(T) / allocator.destroy(ptr) for single items. Allocation can fail, so it returns an error union. YesBuilt-in alloc and free keywords. alloc(expr) returns a typed pointer (*T) and may abort or yield a nullable pointer on failure; free(p) releases it. Manual, C-like, no GC. Yesnew(T) / free(ptr) for single values and make/delete for dynamic containers. These route through the implicit context.allocator, so the same call can target different allocators. PartialANS Forth has the optional MEMORY-ALLOCATION wordset: ALLOCATE ( u -- addr ior ) and FREE ( addr -- ior ). Classic Forth instead grows the dictionary with ALLOT; a general heap is not guaranteed on every system.
Reallocation / resizingGrow or shrink an existing allocation, possibly moving it in memory. Yesrealloc(p, newsize) resizes in place when possible or copies to a new block. Must assign the result back (it may move) and check for NULL before discarding the old pointer. PartialNo realloc for new-allocated memory (it can't run move/copy constructors). Instead std::vector reallocates and relocates elements internally on push_back/resize; raw resizing means manual new + copy + delete. PartialNo dedicated ReAlloc primitive; the idiom is MAlloc a new block, MemCpy the old contents, then Free the original. Some array helpers wrap this pattern. Yesallocator.realloc(old_slice, new_len) resizes (returns a new slice, may move). allocator.resize(slice, n) attempts an in-place resize and returns false if it can't, leaving the original untouched. Yesappend/insert/delete on slices grow and shrink the backing allocation automatically. For raw pointers the pattern is alloc-new + copy + free, as Hare's alloc targets a fixed size. YesDynamic arrays ([dynamic]T) auto-grow via append. The low-level mem.resize/allocator .Resize mode reallocates a raw block through context.allocator. PartialThe MEMORY-ALLOCATION wordset provides RESIZE ( addr u -- addr' ior ), which may move the block. Only available where the heap wordset is implemented; ALLOT-based dictionary memory cannot be resized.
Stack allocationAutomatic storage that is freed when the enclosing scope exits. YesLocal variables live on the stack and are reclaimed at scope exit. C99 adds variable-length arrays (int a[n];) and the nonstandard alloca() for dynamic stack frames. YesAutomatic-storage objects are the heart of RAII: their destructors run deterministically at scope exit. Most C++ objects should live on the stack, owning heap resources indirectly. YesLocal variables are stack-allocated like C. HolyC functions use the normal call stack; locals vanish when the function returns. YesLocals and fixed-size arrays are stack-allocated with no allocator needed. A FixedBufferAllocator can even back a heap-style Allocator with a stack buffer for zero heap use. YesLocal variables and fixed arrays use automatic stack storage and need no free. Only alloc'd memory must be released. YesLocal variables and fixed arrays are stack-allocated and require no allocator. Only new/make touch the context allocator. PartialForth's data and return stacks hold cells/addresses, not arbitrary buffers. Scratch buffers are usually carved from the dictionary with ALLOT or from a PAD/transient region rather than a true scoped stack frame.
Explicit allocator parametersFunctions take the allocator as an argument so allocation is visible at the call site. NoThere is no allocator abstraction in standard C - malloc is a fixed global. Libraries sometimes pass function pointers (alloc/free callbacks) by hand, but the language has no built-in concept. PartialContainers take an Allocator template parameter (std::vector<T, Alloc>), and C++17 adds std::pmr polymorphic allocators passed as runtime arguments. It is opt-in per type, not pervasive. PartialMAlloc accepts an optional heap-pointer argument so you can allocate from a specific task's heap, but there is no general pluggable allocator interface like Zig's. YesThe defining idiom: any function that allocates takes allocator: std.mem.Allocator explicitly. "No hidden allocations" - you always see at the call site who allocates and from where. PartialHare's alloc/free use a single global heap rather than passed-in allocators; there is no first-class Allocator parameter convention as in Zig or Odin. YesAllocators are carried implicitly in the context but are fully explicit when you want: new(T, my_allocator), make(...), or context.allocator = arena to redirect a whole call tree. NoForth has no allocator abstraction. Words like ALLOCATE/ALLOT act on a single global heap or the dictionary; there is no notion of passing an allocator.
Arena / region allocatorsAllocate from a region and free everything at once by resetting the region. Via libraryNot built in, but a trivial and common idiom: bump a pointer through one big malloc'd block and free the whole block at the end. Many libraries (e.g. obstacks, apr_pool) provide arenas. Via librarystd::pmr::monotonic_buffer_resource is a standard-library arena: allocate forward, release everything on destruction. Custom arena allocators are also common. Via libraryNo standard arena type, but the per-task heap effectively acts as a region: when a task dies its heap is reclaimed. Manual arenas are easy to build over MAlloc. Yesstd.heap.ArenaAllocator wraps any backing allocator; you allocate freely and call arena.deinit() (or _ = arena.reset(.free_all)) to release everything at once - no per-object frees. Via libraryNo standard arena in the language; you build one over alloc (bump a slab, free the slab). Hare's minimal stdlib leaves region allocation to user code. YesFirst-class: mem.Arena / virtual.Arena plus context.temp_allocator (a per-thread scratch arena). Set context.allocator = arena_allocator and free the whole arena in one call. PartialThe dictionary itself is an arena: ALLOT bumps HERE, and you can record a mark and later reclaim back to it (where MARKER/EMPTY or saving HERE is supported). Granularity is coarse and global.
Pool / bump allocatorsFixed-size object pools or pointer-bumping allocators for fast, predictable allocation. Via libraryHand-rolled freelists/object pools are a classic C technique; not in the standard library but ubiquitous in practice for fixed-size allocations. Via librarystd::pmr::unsynchronized_pool_resource / synchronized_pool_resource provide standard pool allocators; bump allocation is monotonic_buffer_resource. Many engines ship custom pools. Via libraryNo built-in pool type; pools are easily implemented over MAlloc with a freelist. TempleOS code often used fixed arrays as ad-hoc pools. Yesstd.heap.MemoryPool(T) is a standard fixed-size object pool, and FixedBufferAllocator is a pure bump allocator over a buffer (alloc just advances a pointer; no individual free). Via libraryNot provided by the stdlib; build a bump or pool allocator over a slab obtained from alloc. Simple to do given Hare's slices. Yesmem.Dynamic_Pool / pool allocators and the bump-style mem.Arena ship in core:mem. The temp allocator is a fast bump allocator reset per frame. PartialBump allocation is native via ALLOT advancing HERE. Object pools are typically built as CREATEd cell arrays plus a hand-managed freelist.
RAII / deterministic destructorsResources are released automatically and deterministically when an object's scope ends. NoNo destructors and no scope-bound cleanup. GCC/Clang offer a non-standard __attribute__((cleanup)), but standard C requires manual free/goto cleanup patterns. YesRAII is the signature C++ idiom: a destructor (~T()) runs deterministically at scope exit (or on delete), in reverse construction order, even during exception unwinding. Resources bind to object lifetime. NoHolyC is procedural C-style with no classes or destructors; cleanup is fully manual via Free. NoBy design Zig has no destructors and no RAII ("no hidden control flow"). Deterministic cleanup is done explicitly with defer/errdefer instead. NoNo destructors or RAII. Hare uses defer for scope-exit cleanup, run explicitly by the programmer. NoNo destructors or RAII. Cleanup is explicit via defer (and resetting the context allocator/arena). NoNo objects, scopes, or destructors. All resource cleanup is manual and explicit.
defer / errdefer cleanupSchedule cleanup code to run on scope exit (or only on error exit). NoNo defer. The conventional substitute is goto cleanup; labels, or the non-standard __attribute__((cleanup)) extension. PartialNo defer keyword; RAII destructors cover the same need. A scope_guard / gsl::finally helper (lambda run in a destructor) gives ad-hoc defer-like behavior. NoNo defer construct; cleanup is manual, typically via explicit Free calls or goto-style flow. Yesdefer expr; runs at scope exit in reverse order; errdefer expr; runs only when the scope exits via an error. This is Zig's primary deterministic-cleanup mechanism in place of RAII. Yesdefer expr; schedules an expression to run at scope exit (reverse order). Hare has no separate errdefer; error handling uses explicit propagation. Yesdefer stmt runs at scope exit in LIFO order. Combined with or_return/or_else for error flow; there is no dedicated errdefer. NoNo defer mechanism; cleanup words are called explicitly. CATCH/THROW provide exception-style unwinding but not scheduled deferred cleanup.
Smart pointers / unique & shared ownershipOwning pointer types that automate freeing and express single vs shared ownership. NoNo language support; ownership is conventional and documented, not enforced. Pointers are raw. Yesstd::unique_ptr<T> is single-owner (move-only, frees on destruction); std::shared_ptr<T> is reference-counted shared ownership; std::weak_ptr<T> breaks cycles. The modern way to own heap memory. NoNo smart-pointer types; only raw pointers managed with MAlloc/Free. NoNo smart pointers; pointers and slices are raw. Ownership is a convention, with defer/errdefer for freeing. (Detection tools like GeneralPurposeAllocator catch leaks/use-after-free at runtime.) NoNo smart pointers. Pointers (*T) and nullable pointers (nullable *T) are raw, freed manually with free. NoNo smart pointers in the language; pointers are raw and freed via the context allocator. Ownership is conventional. NoNo pointer types at all - only raw cell addresses. No ownership abstractions.
Reference countingTrack an object's owners and free it when the count reaches zero. Via libraryNo built-in refcounting, but a common manual pattern: store an int refcount, with retain()/release() helpers (e.g. GLib's GObject, COM, kernel kref). Yesstd::shared_ptr<T> provides thread-safe (atomic) reference counting out of the box, with std::weak_ptr for non-owning references to break cycles. NoNo reference counting; lifetimes are managed manually with MAlloc/Free. Via libraryNot built in, but trivially implemented (a struct with a count and retain/release). Some libraries provide Rc/Arc-style wrappers; the std lib does not mandate one. Via libraryNo language refcounting; implement it manually with a counter field and explicit free at zero. Hare's philosophy favors explicit lifetimes. Via libraryNo built-in refcounting type; build one with a counter and the context allocator, or use a community package. Arenas often make refcounting unnecessary. NoNo objects to count; any refcounting would be a fully hand-built cell-and-word scheme, which is rare.
No garbage collector (by design)There is no automatic tracing/reclaiming GC; lifetimes are programmer-controlled. YesNo GC. Memory is fully manual via malloc/free. (Conservative collectors like Boehm GC exist as opt-in libraries but are never the default.) YesNo GC by design; lifetimes are deterministic via RAII, smart pointers, and manual new/delete. (The C++11 GC support API was minimal and removed in C++23.) YesNo GC. TempleOS uses cooperative tasks each with their own heap; memory is reclaimed manually with Free or wholesale when a task ends. YesNo GC, ever - a core design goal. "No hidden allocations": every allocation goes through an explicit Allocator and every free is your responsibility (aided by defer). YesNo GC and no runtime to speak of. Memory is manual via alloc/free, with defer for cleanup - deliberately simple and predictable. YesNo GC. Odin leans on explicit allocators (the context system, arenas, temp allocator) so reclamation is bulk and deterministic, not traced. YesNo GC. Memory is the raw dictionary plus the optional heap wordset; the programmer controls HERE, ALLOT, ALLOCATE, and FREE directly.
Zeroed vs uninitialized memoryControl over whether freshly allocated memory is zero-filled or left with garbage. Yesmalloc returns uninitialized memory; calloc returns zero-filled memory. Reading uninitialized bytes is undefined behavior, so zeroing is your call (memset). Yesnew T default-initializes (leaving trivial types indeterminate), while new T() / new T{} value-initializes to zero. std::vector value-initializes its elements. Fine-grained control of both. YesLike C, MAlloc(size) returns uninitialized memory while CAlloc(size) returns a zero-filled block (CAlloc just calls MAlloc then MemSet(res,0,size)). You can also zero a buffer yourself with MemSet. YesAllocator memory is undefined until written; you opt into garbage with = undefined. std.mem.zeroes(T), @memset(buf, 0), and allocator.alloc + explicit init give precise control. Yesalloc(value) initializes the new object to that value, so it's never garbage. Array literals like [0...] zero-fill; explicit values give other patterns. YesOdin zero-initializes by default - new/make return zeroed memory (the zero value is meaningful). Opt out with mem.alloc(..., zero = false) / --- uninitialized literal for speed. PartialALLOT and ALLOCATE return memory with indeterminate contents; you zero it yourself with ERASE ( addr u -- ) or FILL. No automatic clearing.
Alignment control & aligned allocationRequest memory or types with a specific byte alignment (e.g. for SIMD or hardware). YesC11 adds aligned_alloc(align, size) and _Alignas/_Alignof. POSIX has posix_memalign. Over-alignment is fully supported. Yesalignas(N) / alignof(T), over-aligned new (C++17 routes to aligned operator new), and std::aligned_alloc. Allocators can honor alignment requirements. YesTempleOS provides MAllocAligned(size, alignment, ...) and CAllocAligned(...) (power-of-two alignments) which over-allocate and round the pointer up; plain MAlloc already returns memory suitably aligned for general use. YesAlignment is part of the type system (*align(64) T, []align(16) u8). allocator.alignedAlloc(T, alignment, n) returns over-aligned memory; @alignOf/@alignCast round it out. Partialalign(T) (alignof) and size(T) are built in, and allocations are naturally aligned for the type. There's no dedicated over-aligned allocator call in the core; you over-allocate and align manually. Yes#align(N) struct/field attribute and align_of(T). Allocator calls take an alignment parameter (mem.alloc(size, alignment)), so aligned allocation is first-class. PartialALIGN advances HERE to a cell boundary and ALIGNED ( addr -- a-addr ) rounds an address up; CELL/CHARS give sizes. Stronger (SIMD-style) alignment is manual arithmetic.
Placement / in-place constructionConstruct an object into memory you already allocated, separating allocation from initialization. PartialC has no constructors, so "placement" is just writing into a buffer (memcpy/assignment) or casting a void* to your struct type. Separation of alloc and init is the norm. YesPlacement new (ptr) T(args) constructs an object into existing storage (running the constructor); pair with an explicit p->~T() destructor call. The basis of containers and std::pmr. PartialNo constructors; placing an object is casting a pointer into a MAlloc'd/stack buffer and assigning fields, as in C. YesNo constructors, so you simply write the value into allocated memory: const p = try a.create(T); p.* = T{ ... };, or initialize a slot in a buffer directly. Allocation and init are always separate and explicit. Yesalloc(value) constructs-in-place by initializing the new allocation to value; you can also write through a *T into existing storage. No hidden constructors. YesAssign a composite literal through a pointer into existing memory: (^Node)(buf)^ = Node{...}. No constructors, so init is always an explicit write you can target anywhere. PartialAll construction is in-place by nature: CREATE lays out a name, , and ! store cells at HERE/addresses. There are no constructors, only stores into chosen addresses.
Custom / overridable global allocatorReplace the program-wide allocator to instrument, pool, or redirect all allocations. PartialNo standard hook, but in practice you override by interposing malloc/free at link time (strong symbols, LD_PRELOAD, --wrap), as jemalloc/tcmalloc/mimalloc do. Not language-level. YesReplaceable operator new/operator delete (global and class-scoped) are part of the standard, and std::pmr::set_default_resource swaps the default polymorphic allocator at runtime. PartialNo standard override hook, but TempleOS is fully open and JIT-compiled, so the source-level MAlloc/Free and the task heap structures can be patched or replaced directly. YesThere is no single global allocator to override - by design you choose and pass the Allocator explicitly everywhere, so swapping GeneralPurposeAllocator, page_allocator, an arena, etc. is the normal workflow. Partialalloc/free use a global heap; overriding it isn't a first-class language feature (no allocator parameter), though the runtime's allocator can be replaced at the implementation level. YesSet context.allocator (and context.temp_allocator) to any allocator, and it propagates to all callees via the implicit context - so you redirect every allocation in a scope without touching call sites. PartialALLOT/ALLOCATE and HERE are system words; you can redefine them or revector the dictionary/heap pointers in many Forths, but there's no portable standard override mechanism.

Memory Safety

FeatureCC++HolyCZigHareOdinForth
Array/slice bounds checkingDoes indexing past the end of an array or slice get caught, and when? NoRaw pointer/array indexing is never bounds-checked. a[i] is just *(a + i); an out-of-range index is undefined behavior. There is no length carried with an array that decays to a pointer. Detection only happens externally via ASan or Valgrind. Partialoperator[] on std::vector/std::array/std::span is unchecked (UB), but .at() throws std::out_of_range. C-style arrays and raw pointers are never checked. Hardened standard libraries (libc++ _LIBCPP_HARDENING_MODE, MSVC _ITERATOR_DEBUG_LEVEL) can add checks opt-in. NoA C dialect for TempleOS with the same pointer-array model as C - a[i] is unchecked pointer arithmetic. Code runs in ring 0 with no memory protection, so an overrun can corrupt the kernel or any process. No sanitizers exist for the platform. YesSlices and arrays carry a length, and indexing is bounds-checked at runtime in Debug and ReleaseSafe, panicking with index out of bounds on violation. Checks are elided for speed in ReleaseFast/ReleaseSmall. Many out-of-range indices on comptime-known lengths are caught at compile time. YesSlices and arrays carry their length and are bounds-checked at runtime; an out-of-range index aborts the program. Out-of-range indices into fixed arrays with statically-known bounds are rejected at compile time. YesBounds checking is on by default for arrays, slices, and dynamic arrays, panicking on violation. It can be disabled per-scope with #no_bounds_check or globally with the -no-bounds-check flag for hot paths. NoThere are no arrays as a typed concept - memory is raw cells you reach with @ (fetch) and ! (store) at addresses computed by hand. No length is tracked and nothing is checked; an out-of-range address just reads or writes whatever is there.
Null / nil pointer safetyIs the null pointer a distinct, checked state, or a value any pointer can silently hold? NoAny pointer can be NULL, and dereferencing it is undefined behavior (typically a segfault, but not guaranteed). The type system does not distinguish nullable from non-null pointers; checks are manual if (p) guards. PartialRaw pointers and nullptr behave like C - dereferencing null is UB. References (T&) cannot legally be null, and smart pointers help, but there is no non-nullable raw pointer type. Libraries like gsl::not_null add opt-in checks. NoSame as C: any pointer may be NULL and dereferencing it is unchecked. With no MMU protection in TempleOS, a null deref reads/writes address 0 rather than faulting cleanly. YesPlain pointers (*T) cannot be null. Nullability is opt-in via optionals (?*T), and you must unwrap them (if (p) |v|, orelse, .?) before use. Unwrapping a null with .? panics in safe builds instead of silently dereferencing. YesPointers are non-nullable by default; null is a separate nullable *T type that must be explicitly matched/checked before dereference. The compiler forbids dereferencing a nullable pointer without narrowing it first. PartialPointers default to nil and can be nil; dereferencing nil faults. There is no non-nullable pointer type, but multi-pointers and references aside, the idiom is explicit if p != nil checks. Maybe(T) exists for optional values rather than null pointers. NoAddresses are just integers on the stack; 0 (or any address) can be fetched/stored with no concept of null safety. There is no type system to distinguish a valid pointer from a bogus one.
Use-after-free protectionIs dereferencing a pointer to already-freed memory detected or prevented? NoUsing a pointer after free() is undefined behavior and not detected by the language; the memory may be reused, giving silent corruption. Detection is only via ASan (heap-use-after-free) or Valgrind. PartialNo language-level prevention for raw delete/free, but RAII and smart pointers shrink the window: unique_ptr enforces single ownership and weak_ptr lets you check a shared_ptr's liveness before locking. Dangling references/iterators are still UB; ASan detects misuse at runtime. NoFree() returns memory to the heap with no tracking; using the pointer afterward is unchecked. No sanitizers exist, and ring-0 execution means UAF can corrupt the whole system. PartialNot prevented by the type system, but the GeneralPurposeAllocator (debug builds) detects use-after-free by default and reports it with a stack trace, and std.testing.allocator enforces it in tests. In ReleaseFast no checks run, so the guarantee depends on the allocator and build mode. NoManual free() with defer for ordering, but no built-in UAF detection - using a freed pointer is undefined. Because Hare compiles via QBE to native code, Valgrind/ASan-style tooling can catch it externally, but nothing is built in. PartialNo language-level prevention, but the standard library ships a mem.Tracking_Allocator and Odin supports -sanitize:address (ASan), which catch use-after-free at runtime in instrumented/debug builds. Arena/temp allocators also sidestep per-object frees entirely. NoMemory carved with ALLOT/HERE is typically never individually freed (the dictionary grows like a stack), and any custom heap is hand-rolled with no liveness tracking. Reusing a stale address is just an ordinary fetch/store.
Double-free protectionIs freeing the same allocation twice caught? NoCalling free() twice on the same pointer is undefined behavior. Modern glibc may abort with double free or corruption as a hardening measure, but this is implementation-defined, not a language guarantee. ASan/Valgrind detect it reliably. PartialDouble delete on a raw pointer is UB, but unique_ptr resets to null on move/release and shared_ptr reference-counts, so smart-pointer ownership makes accidental double-free hard. Raw new/delete and malloc/free carry C's risks. NoFree() twice corrupts the TempleOS heap with no detection. No hardened allocator or sanitizer is available on the platform. PartialNot prevented statically, but the GeneralPurposeAllocator detects double-free in safe/debug builds and panics with Double free detected. The testing allocator enforces this too. Unsafe in ReleaseFast unless the allocator still checks. Nofree() performs no double-free guard of its own; freeing twice is undefined. External ASan/Valgrind tooling over the QBE-generated binary is the practical detector. PartialThe default allocator does not guard against it, but the Tracking_Allocator records allocations and reports bad/double frees, and -sanitize:address catches them at runtime. Arena allocators avoid the problem by freeing in bulk. NoThere is no standard free, so double-free is not even a defined operation; any hand-written heap must implement (and police) its own free list with no help from the language.
Dangling-pointer preventionAre pointers to expired stack/heap objects prevented or flagged? NoReturning the address of a local, or keeping a pointer past its object's lifetime, is undefined behavior with no language check. Some compilers warn (-Wreturn-local-addr) and ASan's stack-use-after-return/stack-use-after-scope catch many cases at runtime. PartialSame lifetime hazards as C for raw pointers/references, but RAII ties object lifetime to scope and smart pointers make ownership explicit. Compilers add -Wdangling/lifetime warnings, and [[clang::lifetimebound]] plus the C++ Core Guidelines lifetime profile flag many cases; not a hard guarantee. NoIdentical to C - pointers to dead locals or freed memory dangle silently, with no warnings or sanitizers on TempleOS. PartialNo borrow checker, so dangling stack pointers are possible, but the GPA detects heap dangling (use-after-free) in safe builds, and defer/errdefer make scope-tied cleanup explicit. Pointers to comptime/temporaries are often caught at compile time. NoManual lifetimes with defer for cleanup ordering, but no borrow checking - a pointer can outlive its target undetected. External memory tooling is the only safety net. PartialNo borrow checker; dangling pointers are possible. But defer ties frees to scope, arena/temp allocators make whole-region lifetimes explicit, and -sanitize:address catches heap dangling at runtime. NoAddresses are bare integers with no associated lifetime, so 'dangling' isn't even a tracked notion - you simply must not reuse an address whose backing memory has been reclaimed.
Buffer-overflow exposureHow exposed is the language to writing past a buffer (e.g. via copies/strings)? NoHighly exposed: strcpy, gets, sprintf, and manual loops happily write past buffers. Mitigations are opt-in libc hardening (_FORTIFY_SOURCE), stack canaries (-fstack-protector), and ASan, not part of the language. PartialStill exposed via C arrays and pointer copies, but std::string/std::vector grow automatically and std::span/iterators discourage raw pointer arithmetic. .at(), hardened libs, _FORTIFY_SOURCE, and stack protectors reduce risk; raw buffers remain unsafe. NoAs exposed as C, and worse in impact: with no memory protection in ring 0, an overflow can overwrite kernel structures. No FORTIFY, canaries, or sanitizers exist. YesLength-carrying slices plus runtime bounds checks in Debug/ReleaseSafe turn most overflowing writes into clean panics. There are no overflowing C string functions in the standard idiom; copies use slices of known length (@memcpy checks length equality). YesBounds-checked slices and strings (which carry length) make accidental overflow a runtime abort rather than silent corruption. There is no strcpy-style unchecked primitive in idiomatic Hare. YesDefault bounds checking on slices/arrays plus length-carrying strings and copy(dst, src) (which copies min(len(dst), len(src))) prevent typical overflows. Disabling bounds checks reintroduces the exposure. NoCMOVE/MOVE copy exactly the cell count you give with zero validation against the destination size; raw ! to a computed address can land anywhere. Fully exposed.
Uninitialized-read protectionAre reads of memory that was never initialized prevented or flagged? NoLocals and malloc'd memory hold indeterminate values; reading them is undefined behavior with no language check. Compilers may warn (-Wuninitialized, -Wmaybe-uninitialized); MSan/Valgrind detect it at runtime. calloc zero-initializes. PartialSame hazard for raw scalars and malloc/new[] of trivial types, but value-initialization (int x{};), constructors, and std::optional/std::vector zeroing make 'always initialized' the idiomatic default. Warnings plus MSan catch the rest. NoLocals are uninitialized like C, with no warnings or MemorySanitizer available on TempleOS. YesVariables must be initialized; reading one before assignment is a compile error. To opt into uninitialized memory you must write undefined explicitly, and in safe builds undefined is filled with the poison byte 0xAA to surface accidental reads. PartialDeclarations require an initializer (default values zero-initialize), so accidental uninitialized locals are largely designed out. Heap memory from alloc is initialized to the value you give; raw uninitialized buffers are an explicit choice. PartialVariables are zero-initialized by default (x: int is 0), eliminating the classic garbage read. You opt out explicitly with x: int = --- (no zeroing), which reintroduces uninitialized reads on purpose. NoMemory from ALLOT is not cleared - it contains whatever was previously in the dictionary space. Fetching before storing returns stale bytes, with no detection.
Memory-leak detectionCan the language/runtime detect allocations that were never freed? Via libraryNo GC and no built-in detection - leaks are silent. They are found with external tools: Valgrind/Memcheck, LeakSanitizer (bundled with ASan), or heap profilers. Nothing in the language tracks ownership. PartialNo GC, but RAII and smart pointers make leaks the exception rather than the rule - an object owned by unique_ptr/vector is freed deterministically at scope end. Raw new can still leak; LeakSanitizer/Valgrind catch those. NoMAlloc without Free leaks with no tracking, and no leak-detection tooling exists for TempleOS. TempleOS does reclaim a task's heap when the task exits. YesThe GeneralPurposeAllocator reports leaks on deinit() (with allocation stack traces) and std.testing.allocator fails any test that leaks - leak detection is built into the standard allocators rather than requiring an external tool. Via libraryManual free/defer with no built-in leak detector; an unfreed allocation is silent. Because Hare emits native code, Valgrind/LeakSanitizer can be run over the binary to find leaks externally. PartialThe standard library's mem.Tracking_Allocator records every allocation and reports those still live at shutdown (with source locations), giving built-in leak detection without a GC. -sanitize:address also includes LeakSanitizer. NoALLOT grows the dictionary monotonically and is normally reclaimed only by FORGET/MARKER; there is no allocator bookkeeping to detect leaks in any hand-rolled heap.
Integer-overflow behaviorWhat happens when arithmetic overflows a fixed-width integer? NoSigned integer overflow is undefined behavior (the compiler may assume it never happens); unsigned overflow wraps modulo 2^N (defined). UBSan's signed-integer-overflow check catches the signed case at runtime. NoSame rules as C - signed overflow is UB, unsigned wraps. C++26 adds the C23 ckd_add/ckd_sub/ckd_mul checked-arithmetic functions (from <stdckdint.h>), and UBSan flags signed overflow, but plain +/* are unchecked. NoC-style fixed-width arithmetic with no overflow checking and no UBSan; behavior follows the underlying machine wrap, but is not a language guarantee. YesOverflow with plain +/* is detected: it panics in Debug/ReleaseSafe (integer overflow). Wrapping and saturating are explicit, separate operators (+%, +|), and @addWithOverflow returns a tuple - so you choose the semantics. NoOverflow is defined (unlike C's signed UB): both signed and unsigned arithmetic wrap modulo 2^N via two's-complement truncation. It is not trapped or checked by default - plain +/* wrap silently, the same defined-but-unchecked behavior as Odin. Hare's win over C here is removing the UB, not adding an overflow trap. NoFixed-width integer arithmetic wraps (two's-complement) by default and is not checked; there is no UB like C's signed case, but also no automatic trap. core:math provides explicit checked/saturating helpers when needed. NoSingle-cell arithmetic wraps modulo the cell width with no checking. Mixed/double-cell words (M*, */) exist to widen results manually, but ordinary +/* silently wrap.
Undefined-behavior surfaceHow large is the set of operations whose result the spec leaves undefined? NoVery large UB surface: signed overflow, OOB access, UAF, double-free, strict-aliasing violations, data races, null deref, uninitialized reads, shift-by-width, and more are all undefined. Compilers exploit UB for optimization, so consequences can be non-local. NoInherits C's UB and adds more (invalid downcasts, calling through a dangling this, violating constexpr rules, etc.). RAII/smart pointers shrink how often you hit it, but the spec's UB surface is even larger than C's. NoC-dialect UB surface, made more dangerous by ring-0 execution with no MMU protection - UB that would 'just' segfault elsewhere can instead silently corrupt the OS. PartialZig has 'Illegal Behavior' that is checked and panics in safe builds (overflow, OOB, null unwrap, reaching unreachable) but becomes true UB in ReleaseFast. The philosophy of 'no hidden control flow / no hidden allocations' and explicit undefined keeps the surface smaller and more visible than C. PartialA deliberately small, stable spec with fewer dark corners than C: bounds and many error conditions abort rather than being undefined. Manual memory still leaves UAF/lifetime issues as genuinely undefined. PartialSmaller surface than C: integer overflow is defined (wraps), bounds checks are on by default, and there is no strict-aliasing trap-for-optimization rule of C's kind. Manual memory means UAF/dangling remain undefined unless caught by ASan/tracking. NoThe standard leaves a great deal implementation-defined or undefined (stack underflow/overflow, invalid addresses, environmental dependencies), and the language gives you raw machine access with essentially no guardrails.
Sanitizers & dynamic tooling (ASan/UBSan/Valgrind)What instrumentation/tooling is available to catch memory bugs at runtime? Via libraryBest-in-class external tooling via the toolchain: AddressSanitizer (UAF, OOB, double-free), UndefinedBehaviorSanitizer, MemorySanitizer (uninit reads), ThreadSanitizer, LeakSanitizer, and Valgrind. Enabled with flags like -fsanitize=address,undefined. Via librarySame Clang/GCC sanitizer suite (ASan/UBSan/MSan/TSan/LSan) and Valgrind, plus hardened standard libraries and _GLIBCXX_DEBUG/_ITERATOR_DEBUG_LEVEL for checked iterators and containers. NoNo sanitizers or Valgrind - TempleOS is a self-hosted ring-0 environment with no such tooling; debugging is by inspection and the built-in debugger. PartialMuch detection is built in (GPA's UAF/double-free/leak checks, safe-mode panics for OOB/overflow/null). Externally, Zig also integrates Clang sanitizers - -fsanitize-c, plus ThreadSanitizer support - and works under Valgrind, complementing the in-language checks. Via libraryNo first-party sanitizer flags, but Hare compiles to native ELF (via QBE), so standard Valgrind and, where the ABI cooperates, ASan-style tooling can be run over the produced binary. PartialOdin exposes -sanitize:address, -sanitize:memory, and -sanitize:thread (LLVM sanitizers) plus the built-in mem.Tracking_Allocator for leak/bad-free reports; binaries also run under Valgrind. NoNo dedicated sanitizers; you rely on the interpreter's own stack-depth checks (if any) and manual .S stack inspection. A Forth hosted on a C runtime could be run under Valgrind, but nothing is Forth-aware.
Safe vs unsafe slices / viewsDo fat pointers/slices carry length and stay bounds-checked, or are views raw? NoNo slice type - a 'view' is a bare pointer (optionally with a separately-passed length you must track by hand). Nothing ties the pointer to its length or checks accesses. Partialstd::span and std::string_view are length-carrying views, and std::vector/std::array know their size - but span::operator[] is unchecked by default (UB on OOB), only .at()/hardened modes check. So views carry length, but bounds-checking is opt-in. NoNo slice/view abstraction; the same raw pointer + manual length model as C, with no checks. YesSlices ([]T) are fat pointers carrying .ptr and .len, and indexing/slicing them is bounds-checked in safe builds. Sub-slicing (buf[a..b]) is checked too. Many-item pointers ([*]T) are the explicit unsafe opt-out when you want a raw, length-less pointer. YesSlices ([]T) carry a length and are bounds-checked at runtime; slicing expressions and indexing both validate against the length. Raw pointers are a separate, explicit choice. YesSlices ([]T) are a pointer+length and are bounds-checked by default; s[a:b] slicing is checked too. Raw, length-less access uses multi-pointers ([^]T), the explicit unsafe view. #no_bounds_check opts a region out. NoThe closest idiom is the addr len pair convention (e.g. counted/c-addr u strings) passed on the stack, but it is just two numbers with no enforcement - every access is a raw @/! against an address you compute.

Runtime & Interop

FeatureCC++HolyCZigHareOdinForth
C ABI / FFICall into and expose the platform C ABI without a marshalling layer. YesC is the platform ABI. Declare a prototype and link; no wrapper, no marshalling. Memory crossing the boundary is just raw pointers you malloc/free on whichever side owns it. Yesextern "C" disables name mangling so functions match the C ABI exactly. C structs map 1:1; you can hand a C function a pointer owned by a std::unique_ptr (via .get()), but ownership/free semantics must be agreed manually across the line. PartialHolyC runs only inside TempleOS, which has no libc and its own non-standard ABI, so there is no POSIX C FFI. Within the OS, HolyC and the kernel share one flat 64-bit identity-mapped address space and call each other freely; memory is MAlloc/Free, not malloc/free. YesFirst-class C interop: @cImport parses real C headers at compile time and export/extern + callconv(.C) produce/consume the C ABI directly, no binding generator. C memory is not tracked by Zig allocators, so you free it with the matching C call. Yes@symbol and the rt/FFI facilities bind C symbols directly; Hare emits the SysV C ABI. There is no auto header parsing, so prototypes are hand-declared. Pointers from C are untracked by Hare's alloc/free, so they are released with the C allocator. Yesforeign import blocks bind C libraries with a matching ABI, and procedures marked "c" export the C calling convention. C-allocated pointers bypass Odin's context allocator, so they are freed by the C side that made them. Via libraryThe base standard has no FFI. Hosted systems (gforth, SwiftForth) add libffi-style words (c-library, c-function) to call C; embedded Forths usually expose C via custom primitives. All boundary data is raw addresses you place with ALLOT/ALLOCATE.
No runtime / freestandingRun with no hosted runtime, no GC, and no implicit startup machinery. YesC has no garbage collector and a near-zero runtime. -ffreestanding -nostdlib drops libc and CRT startup entirely; all memory is explicit malloc/free (or your own allocator), nothing runs behind your back. PartialNo GC, and -ffreestanding -fno-exceptions -fno-rtti strips most of the runtime. But destructors (RAII), static-init guards, and operator new still imply runtime support; truly bare C++ means providing those hooks yourself. YesHolyC has no GC and no separate runtime layer - it JIT-compiles straight into the ring-0 TempleOS environment. There is no userspace/kernel split, so code runs freestanding by definition, allocating via the OS heap (MAlloc/Free). YesNo GC and no hidden runtime. freestanding is a real OS target; allocators are values you pass in, so a freestanding build simply supplies its own Allocator (or none). Optional safety checks are the only runtime cost and can be turned off in ReleaseFast. YesNo GC. The +freestanding build tag drops the OS-dependent runtime so Hare runs without a hosted environment; you bring your own allocator and entry point. defer is purely compile-time scheduling, not a runtime service. YesNo GC. -no-crt plus a "freestanding" target removes the C runtime and Odin's default startup. The context (which carries the allocator) is a thin implicit parameter you can set to a freestanding allocator or nil - no hidden heap. YesForth is its own minimal runtime: the inner/outer interpreter plus a dictionary. No GC - memory is the dictionary space grown by HERE/ALLOT. Many Forths (e.g. eForth) boot on bare metal with no OS underneath at all.
Inline assemblyEmbed machine instructions inline, with operands bound to language values. YesCompiler-specific but ubiquitous: GCC/Clang __asm__ with input/output/clobber constraints lets you read and write C variables (including pointers into manually managed memory) directly from assembly. YesSame GCC/Clang asm syntax as C; MSVC offers __asm on x86. Operand constraints bind to C++ lvalues, so you can splice asm around RAII-managed objects, though the asm itself sees only raw addresses. YesHolyC has a built-in inline assembler - asm { ... } blocks accept Intel-syntax instructions and can reference HolyC variables and labels, since the compiler targets x86-64 directly with no abstraction layer. Yesasm volatile (...) is a language primitive with typed input/output/clobber lists that bind to Zig values, used heavily for syscalls and CPU intrinsics in freestanding code. NoHare has no inline-assembly construct. Low-level work is done either through the standard library's syscall wrappers or by linking a separate .s file assembled by the toolchain. YesOdin supports inline assembly via the asm expression with explicit constraint strings, plus rich intrinsics for atomics and SIMD that lower to specific instructions. YesMost native Forths ship an assembler vocabulary: CODE / END-CODE defines a word entirely in assembly. Operands are addresses and stack cells, matching Forth's raw-memory model exactly.
Calling conventionsSelect or specify how arguments, returns, and the stack are passed at the ABI level. YesDefaults to the platform convention (SysV / Win64) and exposes others via attributes like __attribute__((stdcall)) / __cdecl. The convention dictates which args land in registers vs. on the manually managed stack. YesSame convention attributes as C, plus extern "C" to drop mangling. Member functions add a hidden this pointer; overloading is resolved via mangled names, not the convention itself. PartialHolyC uses a single TempleOS-specific convention: all arguments are pushed on the stack (no register-passing), simplifying its JIT. There is no selection of alternate conventions - it is one fixed scheme for the whole OS. Yescallconv(...) is a function-type attribute: .C for the platform ABI, .Naked (no prologue/epilogue) for interrupt/entry stubs, .Interrupt, etc. The default .Auto lets the compiler optimize freely. PartialHare emits the SysV C ABI for export/@symbol functions so it interops cleanly, but the language exposes no syntax to pick alternate conventions (no stdcall/naked); its internal convention is an implementation detail. YesConvention strings on procedure types: "odin" (default, passes the implicit context), "c", "stdcall", "naked", "contextless" (drops the context parameter). Choosing "contextless" also means no implicit allocator is threaded in. NoForth has no register-level calling convention in the source language - words communicate solely through the data and return stacks. A C-style convention only appears at the FFI/CODE-word boundary, handled by the assembler.
Standalone static binariesProduce a single statically linked executable with no shared-library dependencies. Yes-static links libc and all deps into the binary; with musl this yields a tiny, fully self-contained executable. The heap is still malloc/free at runtime - static linking only fixes the code, not memory management. Yes-static -static-libstdc++ -static-libgcc bundles the C++ runtime too. The result carries the standard allocator and exception unwinder; RAII still drives all heap cleanup at runtime. NoHolyC does not produce standalone OS executables at all - it JIT-compiles into a running TempleOS image. Programs are dictionary entries in that single-address-space system, not portable static files. YesStatic linking is the default for many targets, and the bundled musl/compiler-rt make fully static, cross-compiled binaries trivial - no system libc needed. Allocators are still chosen and run at runtime. YesHare statically links by default: its runtime and stdlib are baked in, producing a self-contained executable with no shared-object dependencies on supported platforms. YesOdin emits native objects via LLVM and links a static executable by default; -no-crt can even drop the C runtime for a maximally standalone binary. The context allocator is set up at startup, not loaded from a shared lib. PartialA Forth system is usually a static interpreter binary plus a dictionary image. Some Forths can turnkey/save a sealed image as a standalone executable; others always ship as interpreter + image, so it is system-dependent.
Compile to CEmit portable C source as a backend instead of (or alongside) native code. YesTrivially: C source is the target. It is itself the canonical 'portable assembly' that other languages emit, with its explicit malloc/free memory model. NoModern C++ compiles directly to native code; the old Cfront C-emitting front end is long obsolete. Features like RAII, templates, and exceptions have no clean C equivalent, so there is no mainstream C backend. NoHolyC JIT-compiles straight to x86-64 machine code inside TempleOS. It has no C source backend and no separate compilation phase to emit C from. YesZig has an official C backend (-ofmt=c) that emits C source, useful for bootstrapping and exotic targets. The generated C reflects Zig's explicit-allocator model - no GC is introduced. NoHare's harec compiles to its own intermediate form then to native code via QBE; there is no supported C-source output backend. NoOdin targets LLVM IR (and has experimental non-LLVM backends) but emits no C source. There is no compile-to-C path. PartialNot standard, but exists in practice: some Forth cross-compilers and projects generate C as a portability target. It is a niche tool choice, not a built-in feature of the language.
EmbeddabilityDrop the language into a host application as a library or scripting/extension layer. YesC compiles to ordinary .a/.so libraries that any host links. With no runtime to initialize and caller-controlled malloc/free, C code embeds into anything - it is the lingua franca other runtimes embed through. PartialEmbeds well as a library, but the C++ runtime (static init, exceptions, allocator) and ABI fragility complicate dropping it into arbitrary hosts. Exposing an extern "C" facade is the usual workaround. NoHolyC is inseparable from TempleOS - its compiler is the OS shell. It cannot be embedded into a host application on another platform. YesZig builds static/dynamic libraries with a clean C ABI and no hidden runtime, so it embeds into C, C++, Rust, etc. Because allocators are passed in, the host can supply its own allocator to embedded Zig code. PartialHare can build libraries and export C symbols, so embedding is technically possible, but the ecosystem is young, Linux/BSD-centric, and not designed as a guest runtime, so support is limited in practice. YesOdin compiles to static or shared libraries with C-compatible exports (-build-mode:dll/:static). Embedded procedures should use a "c"/"contextless" convention so the host need not set up Odin's context/allocator. YesForth is a classic embedded scripting layer: tiny interpreters (FICL, pForth, ATLAST) are designed to be linked into a C host as a command/extension language, sharing the host's memory via raw addresses.
Bare-metal / OS-dev suitabilityWrite kernels, bootloaders, and firmware with full control over memory and hardware. YesThe default OS-dev language: freestanding mode, inline asm, fixed-width types, and fully manual memory (you write the allocator the kernel hands out). Linux, BSDs, and most firmware are built on it. PartialUsable for kernels (parts of many OSes are C++), but you must disable exceptions/RTTI and provide operator new/static-init support yourself before RAII works. More moving parts than C in a freestanding setting. YesHolyC was built for OS development - TempleOS itself is written in it. Ring-0 only, single flat identity-mapped address space, inline asm, and direct hardware access; memory is the OS heap via MAlloc/Free. YesStrong fit: freestanding targets, naked functions for ISRs, comptime for register maps, inline asm, and explicit allocators so the kernel owns every byte. Used in real hobby and production kernels. PartialHas the pieces - +freestanding, manual alloc/free, defer, SysV ABI - and toy kernels exist, but no inline asm and a small ecosystem make it less battle-tested than C/Zig for OS work. YesSupports freestanding targets, -no-crt, inline asm, and naked/contextless procedures. For kernel code you typically run --default-to-nil-allocator or set a custom context allocator so nothing implicitly heap-allocates. YesA long bare-metal tradition (Open Firmware, FORTH ROMs, embedded controllers). A native Forth needs only a few KB and boots directly on hardware, managing memory as the raw dictionary via HERE/ALLOT/@/!.

Tooling & Ecosystem

FeatureCC++HolyCZigHareOdinForth
Reference compiler(s)The canonical compiler(s) that turn source into native machine code. YesNo single reference implementation - C is an ISO standard with many mature compilers. GCC and Clang/LLVM dominate on Unix, MSVC on Windows, plus tcc, icc, and embedded vendor toolchains. All emit native code; the language itself adds no runtime memory management, so malloc/free come from the C library, not the compiler. YesAlso ISO-standardized with multiple production compilers: GCC (g++), Clang/LLVM, and MSVC. They compile templates, RAII destructors, and exceptions to native code with the zero-overhead model. Memory is still manual (new/delete), but compilers emit the destructor calls that make RAII work. YesThe reference compiler is the 64-bit JIT built into TempleOS by Terry A. Davis - there is no separate batch compiler. Source compiles to native ring-0 code on the fly as the shell reads it. Independent community compilers/transpilers (e.g. HolyC-Compiler, transpilers to C) target mainstream OSes, but none is a formal standard. YesThe single reference compiler is zig itself, an LLVM-backed self-hosted toolchain with its own bundled backends. It enforces Zig's model where there is no hidden allocation - allocators are values passed explicitly - and lowers defer/errdefer into deterministic cleanup at scope exit. YesThe reference compiler is harec, driven by the hare build tool, currently targeting QBE (with plans for more backends). It compiles to native code with a minimal runtime - no GC and no hidden allocations - so all memory is freed manually, with defer for scope cleanup. YesThe reference compiler is the odin compiler (LLVM backend) maintained by Ginger Bill. It compiles to native code with no RAII and no hidden destructors; allocation routes through the implicit per-scope context.allocator, and defer handles scope cleanup. PartialThere is no single reference compiler - Forth is an ANS/Forth-2012 standard with many small implementations. Most are interactive incremental compilers that compile each new word into the dictionary as you type. Common ones: Gforth (GNU), SwiftForth, VFX. Memory is raw: words like HERE/ALLOT carve out the dictionary directly.
Build systemStandard way to compile, link, and orchestrate a multi-file project. Via libraryNo build system ships with the language. The ecosystem relies on external tools - Make, CMake, Meson, Ninja, autotools - to drive the compiler and linker. Choice and configuration are left entirely to the project. Via librarySame situation as C and historically harder: CMake is the de facto standard, with Meson, Bazel, and Make also common. None is part of the ISO language; the toolchain is assembled externally. PartialThere is no conventional build system - the JIT shell is the build step. You #include files and run them; top-level statements execute as they compile, so a program is effectively built and launched in one act. No external orchestrator exists for the TempleOS environment. Yeszig build is a first-class, built-in build system: you write build.zig in Zig itself (no separate DSL), and it handles compilation, linking, caching, and steps. It can even build C/C++ projects, making zig cc a drop-in compiler/build driver. YesThe hare command is the integrated build tool: hare build, hare run, and hare test compile modules without any external Makefile. Module layout maps directly to the filesystem. YesThe odin compiler is also the build driver: odin build <dir> and odin run <dir> compile a directory (a package) in one invocation. No separate build-system file is required for typical projects. PartialNo build system in the conventional sense - you load source files into a running image with words like INCLUDE/REQUIRE, compiling each definition into the dictionary as it is read. Some systems can save a turnkey image, but there is no standard external build tool.
Package managerOfficial tooling to declare, fetch, and version third-party dependencies. Via libraryNo official package manager. Dependencies are usually system libraries handled by the OS package manager (apt, pacman, etc.) or vendored. Third-party tools like Conan and vcpkg exist but are not part of C. Via libraryNo official manager either, but the ecosystem has stronger third-party options: Conan and vcpkg are widely used to fetch and build dependencies. Neither is endorsed by the ISO standard. NoNo package manager and no real dependency ecosystem - TempleOS is a self-contained, single-developer system with everything bundled. You copy .HC source in directly. YesZig has a built-in package manager integrated with zig build: dependencies are declared in build.zig.zon (with content hashes) and fetched/cached by the toolchain. Still maturing but official. NoDeliberately no package manager. Hare's philosophy favors a small, stable language with vendored or system-provided dependencies; the standard library is intended to cover most needs. NoNo official package manager. Odin encourages vendoring packages into your project tree (and ships a large vendor collection of bindings), rather than fetching remote dependencies. NoNo standard package manager. Code is shared as source files loaded directly; individual systems may have ad-hoc package facilities, but nothing standardized across Forth.
REPL / interactive useAbility to type expressions and run code interactively, line by line. Via libraryC is compile-and-run, with no REPL in the language. Interactive use is possible only through external tools like Cling (a Clang/LLVM-based C/C++ interpreter) or tcc's scripting mode. Via libraryNo native REPL; Cling provides interactive C++ for experimentation and is widely used in scientific computing (ROOT). Not part of standard C++. YesInteractivity is core to the design - the TempleOS shell is a HolyC REPL. Because the compiler is a JIT and there is no required main(), typing an expression or a function name at the prompt compiles and runs it immediately. NoNo official REPL. Zig is an ahead-of-time compiled language; rapid iteration is done via zig run file.zig or zig build run, not an interactive prompt. NoNo REPL. Hare is ahead-of-time compiled; the closest is hare run file.ha to compile and execute a program in one step. NoNo REPL. Odin compiles ahead of time; odin run builds and runs a package immediately but is not an interactive prompt. YesForth is fundamentally interactive - the system is a REPL. You type words at the prompt and they execute (or compile new words) immediately against the shared data stack; the dictionary grows live as you define words.
Debugger supportSource-level debugging: breakpoints, stepping, and inspecting memory. YesExcellent, mature support. Compilers emit DWARF debug info consumed by GDB and LLDB, with full breakpoints, stepping, watchpoints, and raw memory/pointer inspection. Tools like Valgrind and AddressSanitizer help catch manual-memory bugs (leaks, use-after-free). YesSame first-class GDB/LLDB support as C, with pretty-printers for STL containers and smart pointers. ASan/Valgrind are essential for the manual-memory parts that RAII doesn't cover. PartialTempleOS includes an integrated debugger and tools (e.g. trace, Bt backtraces), but there is no memory protection, so a wild pointer or double-free can corrupt the whole ring-0 system rather than trap cleanly. Debugging is in-house to the OS, not via GDB. YesZig emits standard DWARF, so GDB/LLDB work for source-level debugging. The toolchain also provides built-in safety checks in Debug/ReleaseSafe modes (bounds checks, undefined-behavior detection) and integrates an allocator-level leak detector via GeneralPurposeAllocator. PartialHare programs are native and produce debug info usable by GDB, but tooling is young and there are no Hare-specific pretty-printers or sanitizers yet. Manual memory is debugged with general native tools. YesThe LLVM backend emits DWARF/PDB debug info, so GDB, LLDB, and Visual Studio / RAD Debugger work at source level. Odin also ships a built-in tracking allocator for leak and double-free detection of its manual, context-allocator memory. PartialMost Forths offer interactive inspection rather than a source-stepping debugger: words like .S (show stack), SEE (decompile a word), WORDS, and DUMP let you examine the stack and raw memory live. No standard breakpoint debugger.
FormatterOfficial or standard tool to auto-format source to a canonical style. Via libraryNo official formatter. clang-format is the de facto standard (also indent, uncrustify), but style is not part of the language and must be configured per project. Via librarySame as C - clang-format is ubiquitous but external and configurable; there is no canonical style mandated by the standard. NoNo formatter. Code is written in TempleOS's own rich-text .HC editor, and there is no separate canonical-style auto-formatting tool. YesBuilt in: zig fmt enforces one canonical style with no options to bikeshed, mirroring Go's approach. It ships with the compiler. PartialHare has a code style and tooling is emerging, but there is no universally established harefmt shipped as a stable, canonical formatter the way Zig/Odin provide one. Style is largely conventional. PartialOdin ships an odinfmt formatter (developed alongside ols, the language server), but it is community/tooling-side rather than a long-stable core command, so support is solid but not as canonical as zig fmt. NoNo formatter. Forth source is free-form whitespace-delimited words; layout is by convention (e.g. stack-effect comments), with no standard auto-formatting tool.
Cross-compilationBuilding binaries for a target OS/architecture different from the host. YesFully supported, but historically fiddly: you need a cross-toolchain (cross-GCC/Clang) plus target headers, libc, and a sysroot for each target. Capable but the setup is on you. YesSame as C - cross-compilation works with a target toolchain and sysroot, and is widely done for embedded and mobile, but assembling the toolchain (incl. the target's C++ standard library) is the hard part. NoNot applicable - HolyC's JIT targets only TempleOS on 64-bit x86 ring 0. There is no cross-compilation story; the language is tied to its single platform. YesA flagship feature. The compiler bundles libc sources and headers for many targets, so cross-compiling is one flag - zig build-exe -target aarch64-linux - with no external sysroot. zig cc brings the same trivial cross-compilation to C/C++. PartialHare supports multiple targets, but cross-compilation requires the appropriate target toolchain/runtime to be set up; it is supported rather than the one-flag experience Zig offers. YesThe compiler cross-compiles via LLVM with a -target: flag (e.g. -target:linux_arm64); for the easiest results you may still need the target's system libraries/linker, but multiple OS/arch targets are first-class. PartialForth has a specialized form: metacompilation / cross-compilation to build a new Forth image for another (often embedded) machine is a classic technique, but it is system-specific and manual rather than a general one-command toolchain feature.
Standard library breadthHow much functionality ships in the language's standard library out of the box. PartialDeliberately small. The C standard library covers strings, math, I/O, and malloc/free, but has no containers, networking, or threads beyond the C11 threads minimum. Almost everything else comes from the OS or third-party libraries. YesBroad by comparison: the STL provides containers, algorithms, iterators, strings, smart pointers (unique_ptr/shared_ptr for RAII-based memory), threads, filesystem, ranges, and more - all GC-free, with lifetimes managed by RAII. PartialThere is no conventional standard library - instead HolyC programs use TempleOS intrinsics and OS routines (Print, MAlloc/Free, graphics, sound). Rich for what TempleOS does, but tightly coupled to that one OS and not portable. YesA comprehensive, modern standard library: data structures, formatting, I/O, networking, crypto, hashing, JSON, and a full set of allocator implementations (arena, GPA, fixed-buffer) - consistent with Zig's explicit, GC-free memory model. YesHare ships a deliberately self-sufficient standard library (strings, I/O, networking, crypto, date/time, even its own compiler-support code) so projects can avoid external dependencies - reflecting its no-package-manager philosophy. YesOdin's core library is substantial - collections, strings, math/linear algebra, I/O, threads, allocators - plus a large vendor set of bindings (SDL, GLFW, etc.). All allocation flows through the explicit context.allocator, no GC. PartialThe standard defines a core wordset (stack ops, arithmetic, control flow, memory access via @/!, dictionary words) plus optional wordsets, but it is minimal - there are no built-in high-level data structures, networking, or strings beyond the basics. You build up from raw words and memory.
Language serverAn LSP server providing editor completion, diagnostics, and go-to-definition. YesMature LSP support via clangd (Clang-based), giving completion, diagnostics, and navigation across editors. Also ccls. Not part of the language, but rock-solid and ubiquitous. YesSame clangd/ccls servers serve C++ with template-aware completion and diagnostics; widely used in VS Code, Vim, and Emacs. NoNo language server. HolyC is edited inside TempleOS's own integrated editor on a system that predates and ignores LSP; there is no external editor tooling. YesZLS (Zig Language Server) is the well-supported community LSP, providing completion, diagnostics, and go-to-definition. The Zig project also tracks an in-compiler language-server effort. PartialEditor support is young - there are community efforts toward a Hare language server, but no mature, widely adopted LSP comparable to clangd or ZLS. Most tooling is syntax highlighting today. Yesols (Odin Language Server) is a well-established community LSP offering completion, diagnostics, and navigation, paired with the odinfmt formatter. NoNo standard language server. Forth's interactivity (live WORDS, SEE, immediate evaluation) substitutes for static tooling, and the fragmented, image-based ecosystem has no widely adopted LSP.

Type System

FeatureCC++HolyCZigHareOdinForth
Static typingTypes are checked at compile time, not at run time. YesStatically typed: every variable and function has a compile-time type. The checking is weak and easily bypassed - casts and void* silently reinterpret bytes - but there is no runtime type information and no garbage collector behind it; malloc returns untyped storage you ascribe a type to. YesStatically typed with a far stronger checker than C (overload resolution, templates, references). Object lifetime is bound to the static type via RAII, so a destructor runs deterministically when the type goes out of scope - no GC. YesStatically typed C dialect of TempleOS, but essentially unchecked - like C with the safety filed off. No GC; memory is grabbed with MAlloc and returned with Free. YesStatically and strongly typed, with no hidden runtime type machinery and no GC. Allocation is explicit: you thread an Allocator through your code, so even the type of where memory comes from is a value you pass around. YesStatically and strongly typed with a deliberately small spec. No GC: memory is alloc/free and cleanup is sequenced with defer. YesStatically and strongly typed. No GC, but allocation goes through an implicit context.allocator, so new/make are typed yet still under your control; defer orders teardown. NoForth is typeless: the only currency is the machine cell on the data stack. A cell may hold an integer, an address, a character, or a flag - nothing checks which, at compile time or run time. Memory is the raw dictionary: HERE, ALLOT, @, !.
Strong typingThe compiler refuses implicit, lossy, or reinterpreting conversions between unrelated types. PartialWeakly typed: integers, enums, and pointers convert implicitly, and any pointer can be cast to any other. void* and casts let you reinterpret the bytes that malloc handed you with no check at all. PartialStronger than C - no implicit void* conversions, narrowing in {} is rejected - but C-style casts and implicit numeric/derived-to-base conversions remain. reinterpret_cast still reinterprets raw memory on demand. NoWeak, like C, and even looser in practice - implicit conversions are pervasive and the compiler does little to stop you reinterpreting a value or an address. YesStrongly typed: no implicit narrowing or sign changes; widening uses @intCast/@as, and reinterpreting bytes needs @ptrCast/@bitCast. Coercions are spelled out so a pointer never silently becomes another type. YesStrongly typed: conversions between numeric or pointer types are explicit (: type cast). The small, rigid type system is the whole point - fewer silent coercions than C. YesStrongly typed: no implicit numeric conversions, casts are explicit (cast(T)x, transmute(T)x). transmute is the only way to reinterpret bits, and you must ask for it. NoThere is nothing to be strong or weak about - every cell is interchangeable. Treating an address as a number or vice versa is normal Forth, and @/! will fetch or store at whatever address is on top of the stack.
Type inferenceThe compiler deduces a variable's type from its initializer so you needn't write it. PartialHistorically none; since C23 auto infers a local's type from its initializer. Otherwise every declaration, including the type cast on a malloc result, is written out by hand. Yesauto and decltype infer locals and return types; templates deduce argument types. A std::unique_ptr<T> from make_unique is fully inferred, RAII and all. NoNo inference - declarations carry an explicit type (I64, U8, F64, …), just like classic C. Yesconst/var infer from the initializer; you add : T only to force a type or coerce. Even the allocator value you pass around has an inferred type at the call site. Yeslet/const infer the type from the right-hand side; annotate only when you need a specific width or to disambiguate. Yesname := value infers the type; name: T = value is the explicit form. Inference covers new/make results, which still allocate through the context allocator. NoNo types means nothing to infer. The compiler only knows you pushed a cell; what it means is up to the word that consumes it.
Fixed-width integer typesInteger types with a guaranteed, explicit bit width (e.g. 8/16/32/64-bit). YesBuilt-in int/long widths are implementation-defined, but <stdint.h> provides exact-width int32_t, uint64_t, etc. These exact-width spellings are what you reach for when laying out heap structs from malloc. YesInherits <cstdint> (int32_t, uint64_t, …); since C++11 also std::int_fast32_t/int_least32_t families. YesFixed-width types are the primary spelling: I8/U8, I16/U16, I32/U32, I64/U64 (signed/unsigned), plus F64. There is no width-ambiguous int. YesSized integers are first-class and arbitrary: i8i64, u32, and any bit width like u7 or i128. usize/isize track pointer width - relevant when you size allocations. YesExact-width i8/u8 through i64/u64 are built in, alongside int/uint and size. Widths are part of the small core spec, not a header. YesBuilt-in i8i128, u8u128, plus endian-specific variants (u32le, i16be) and uintptr/int. Widths are explicit in the language proper. PartialForth has the single cell (typically 32 or 64 bits, ANS leaves it implementation-defined), plus the double-cell. Sub-cell access is by byte (C@/C!) into raw memory; there is no u16/i32 type, only widths you address yourself.
Sum / tagged unionsA type holding one of several alternatives, with a checked tag selecting which. PartialOnly the untagged union - overlapping memory with no discriminant. Safe use means hand-rolling a struct { int tag; union { … }; } and trusting yourself to read the right member. Via libraryThe raw union is C-style, but std::variant<int, float> (C++17) is a real tagged union: it stores which alternative is active and throws/std::visit-checks access. RAII manages the active member's lifetime. PartialHas C's untagged union only; any tag-and-dispatch discipline is yours to write, with no checking. Yesunion(enum) is a true tagged union; switching on it is exhaustively checked and gives you the payload type-safely. Bare union (untagged) is also available for C interop. YesTagged unions are core: (int | f64 | void) is a checked sum type, matched with match. The error/optional machinery is built on the same tagged-union mechanism. Yesunion {int, f32} is a tagged union with a runtime tag, consumed by a checked type switch. #no_nil and bare union variants exist, but the default carries the tag. NoNo type system, hence no unions of any kind - a cell already 'is' whatever you decide to read it as. Any variant discipline lives entirely in your own words operating on raw memory.
Generics / parametric polymorphismCode that works over many types without sacrificing static type checking. PartialNo real generics. void* plus size_t (the qsort/container idiom) erases types, and _Generic (C11) gives compile-time type selection, but both are workarounds - you manage the typed/untyped boundary, and the heap, by hand. YesTemplates are full compile-time parametric polymorphism, with C++20 concepts to constrain them. std::vector<T> and std::unique_ptr<T> are generic and own their memory via RAII. NoNone - it is a stripped C dialect, so the only generic tool is the same void*-style erasure C offers, used manually. YesGenerics are functions over comptime type parameters - a generic type is just a function returning a type. std.ArrayList(T) is generic and takes an Allocator so you stay in charge of memory. PartialNo user-defined generics by design (the spec stays small). A few intrinsics like len/append/alloc are polymorphic over slices/types, but you cannot write your own parametric container. YesParametric polymorphism via $T type parameters on procedures and structs, with where clauses. Generic containers still allocate through the context allocator. NoWords are inherently 'generic' only because the stack is typeless - a word does the same thing to whatever cells it finds. There is no parametric polymorphism because there are no type parameters, and no types.
Distinct / named typesDefining a new type with the same representation but treated as incompatible. Partialtypedef only aliases - typedef int Meters; is still freely interchangeable with int. The one way to force distinctness is wrapping in a single-field struct, which the compiler treats as its own type. Partialusing/typedef are aliases; enum class is genuinely distinct. True strong typedefs need a wrapper struct or a library (e.g. a strong_typedef template) - no first-class keyword. PartialLike C: classes/structs are distinct, but type aliases are not enforced as separate types. PartialEach struct/enum/opaque is a distinct type, and enums can be non-int. But there is no keyword to make a thin distinct alias of, say, u32; you wrap it in a one-field struct. YesHare has explicit named types: type meters = int; declares a new, distinct type (not just an alias), so meters and plain ints don't mix without a cast. Yesdistinct is a keyword: Meters :: distinct int is a brand-new, incompatible type sharing int's representation - exactly to catch unit/identifier mix-ups. NoNo named types of any kind; a value's 'type' is purely the convention of the word using it, so distinctness cannot exist.
Compile-time types (types as values)Treating types as first-class values manipulable during compilation. NoTypes are never values. The nearest tools - sizeof, _Generic, the preprocessor - operate on types syntactically, not as data you can compute with. PartialTemplate metaprogramming and constexpr/consteval compute heavily at compile time, and traits manipulate types, but a type is not a plain runtime-style value you assign to a variable - it lives in the template/<type_traits> layer. NoNone - no metaprogramming over types beyond C's sizeof. YesThis is Zig's signature feature: type is an ordinary value usable in comptime code, so you pass types to functions, return them, and build new ones - generics, reflection, and @TypeOf all fall out of it. No macros, no separate metalanguage. NoDeliberately minimal: there is no compile-time type computation or metaprogramming. Simplicity over a comptime layer is an explicit design goal. Partialtypeid/type_info_of give compile-time type information and $T parameters specialize at compile time, but types are not freely computed and returned as values the way Zig's comptime type allows. NoForth has powerful compile-time metaprogramming (immediate words, CREATE/DOES>), but it manipulates words and raw cells, not types - there are none.
No-null option typesAn explicit 'maybe absent' type the compiler forces you to handle, instead of null. NoAbsence is the null pointer, unchecked. A malloc that fails returns NULL and nothing forces you to test it before dereferencing. Via librarystd::optional<T> (C++17) is a value-based maybe-type that the API nudges you to check; raw pointers can still be null. optional stores its value inline and destroys it via RAII. NoSame as C - pointers may be null and nothing checks it. YesOptionals are built in: ?T may be null, and the compiler forces you to unwrap it (if (x) |v|, orelse, .?) before use - a plain pointer cannot be null unless its type says ?*T. YesNullable pointers are a distinct type nullable *T; you must check or cast away the nullability before dereferencing. Optionality is expressed through the tagged-union machinery ((T | void)). PartialNo dedicated Option type, but the Maybe(T) union in core and the (value, ok) multiple-return idiom make absence explicit; raw pointers can still be nil. NoAbsence is just a flag or a zero cell you agree to interpret as 'nothing' - no type enforces it.
Struct / record typesAggregating named fields of possibly different types into one value. Yesstruct is the core aggregate, with predictable field layout (padding/alignment rules) - exactly why you can malloc(sizeof(struct T)) and map fields onto raw bytes. Yesstruct/class add methods, constructors, and destructors; RAII means a struct holding owned resources frees them deterministically when it leaves scope - no manual free for members. Yesclass (HolyC's word for a record) and struct-style aggregates exist, with optional member functions and C-like field layout. Yesstruct types are values (and double as namespaces); packed/extern structs control layout for C interop and for memory you allocate yourself. Yesstruct { x: int, y: int } aggregates fields with C-compatible layout; you alloc them and free/defer to release. Yesstruct aggregates fields and supports tags like #packed, #align, and parametric ($T) fields. new(P) allocates one through the context allocator. PartialNo native record type, but the standard CREATE/field-word idiom (or a BEGIN-STRUCTURE extension) builds structs as named byte offsets into raw memory you ALLOT - fields are just + and @/!.