Comparison with Other Languages
SafeC occupies a specific point in the design space of systems programming languages. This page compares it with C, C++, Rust, and Zig across the dimensions that matter most for low-level, safety-critical, and real-time systems.
Feature Comparison
| Feature | C | C++ | Rust | Zig | SafeC |
|---|---|---|---|---|---|
| Memory Safety | No | No | Yes | No | Yes |
| C ABI | Native | Native | FFI | Native | Native |
| Hidden Runtime | No | Partial | Partial | No | No |
| Compile-Time Eval | No | constexpr | const fn | comptime | Compile-time-first |
| Garbage Collector | No | No | No | No | No |
| Unsafe Escape | N/A | N/A | unsafe |
Manual | unsafe {} |
| Memory Model | Manual | Manual/RAII | Ownership | Allocators | Explicit regions |
| Preprocessor | Full (unsafe) | Full (unsafe) | None | Limited | Disciplined subset |
| Exceptions | No (setjmp) | Yes | No (panic) | No | No |
| Generics | No | Templates | Generics | comptime | Monomorphized |
| Struct Methods | No | Classes | impl blocks | Methods | Struct methods |
| Operator Overloading | No | Yes | Traits | No | Yes |
The "Memory Safety" row above is necessarily a single yes/no — see Formal Safety Model's memory-safety comparison for a property-by-property breakdown (spatial safety, use-after-free, double-free, null safety, data races, ...) of what each language actually checks, and whether that checking happens at compile time, at runtime, or as an opt-in build mode.
Compared to C
SafeC is a superset of C. Valid C patterns work in SafeC, but SafeC adds compile-time safety checks that C lacks.
What SafeC adds
Region-annotated references prevent dangling pointers and use-after-free:
// C: compiles fine, undefined behavior at runtime
int* dangling() {
int x = 42;
return &x; // returns pointer to dead stack variable
}
// SafeC: compile-time error
&stack int dangling() {
int x = 42;
return &x; // ERROR: stack reference escapes function scope
}
Bounds checking catches out-of-bounds access:
// C: silent buffer overflow
int arr[4];
arr[10] = 99; // undefined behavior, no diagnostic
// SafeC: compile-time error for constant index
int arr[4];
arr[10] = 99; // ERROR: index 10 out of bounds for array of size 4
Borrow checking prevents aliasing violations:
int x = 10;
&stack int a = &x; // mutable borrow
&stack int b = &x; // ERROR: x is already mutably borrowed
Disciplined preprocessor rejects dangerous macro patterns while keeping useful ones:
// Allowed: object-like macros, #ifdef, #pragma once
#define BUFFER_SIZE 1024
#ifdef DEBUG
// ...
#endif
// Rejected by default (not an opt-in "safe mode" — this is the normal
// compile path; --compat-preprocessor is the opt-in flag that allows it):
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // ERROR unless --compat-preprocessor
// Use: generic<T> T max(T a, T b) instead
What stays the same
- Struct layout is identical
- Calling convention is identical
- Pointer arithmetic works the same
sizeof,alignofbehave the same- C headers work via
#include - Object files link together
Migration path
You can adopt SafeC one file at a time. Rename .c to .sc, fix the diagnostics the compiler reports, and link the resulting object alongside your existing C objects. No wrapper layer, no binding generator.
Compared to C++
C++ and SafeC solve similar problems — adding safety and abstraction to C — but with fundamentally different philosophies about hidden behavior.
Hidden costs in C++
// C++: how many function calls happen here?
std::string greet(std::string name) {
return "Hello, " + name + "!";
}
// Answer: at least 3 allocations, 2 copies or moves,
// 3 destructor calls — none visible in the source
// SafeC: every operation is visible
#include <std/collections/string.h>
// name is borrowed read-only, out is appended to; each append is a
// visible call that may grow out's buffer
int greet(const &String name, &String out) {
return out.push("Hello, ") && out.push(name.as_ptr()) && out.push("!");
}
No implicit special member functions
C++ generates constructors, destructors, copy/move operators implicitly. SafeC does not. Struct creation is always aggregate initialization or field-by-field assignment, usually wrapped in an explicit static method such as File::open(...) that you call yourself. Cleanup is always explicit.
struct Buffer {
?&heap char data; // null until allocated
unsigned long size;
};
// No implicit constructor — you initialize fields explicitly
struct Buffer b;
b.data = std::alloc(1024UL); // T = char, from the field's type
b.size = 1024UL;
// No implicit destructor — you free explicitly
defer { std::dealloc(b.data); }
No exceptions
C++ exceptions add hidden control flow paths, stack unwinding machinery, and unpredictable timing. SafeC has no exception mechanism. Errors are returned as values:
// Error handling is always visible
int result = loadConfig(path, &config);
if (result != 0) {
printf("failed to load config: error %d\n", result);
return result;
}
No RTTI or virtual dispatch
SafeC has no virtual functions, no dynamic_cast, no vtables. Polymorphism is achieved through generics (monomorphized at compile time) or function pointers (explicit) — see Polymorphism & OOP for the full picture, including fn_eval (vtable-free per-type method resolution) and how to build explicit runtime dispatch when you need it.
What SafeC keeps from C++
- Struct methods (with
selfinstead ofthis) - Operator overloading (explicit, opt-in)
- Generics (via monomorphization, not templates)
Compared to Rust
Rust and SafeC share the goal of memory safety without garbage collection, but they achieve it through different mechanisms.
Ownership vs Regions
Rust tracks ownership transfer: a value has one owner at a time, and ownership can be moved or borrowed.
SafeC tracks region membership: a reference knows which memory region it points into, and the compiler prevents references from escaping their region.
// Rust: ownership transfer
fn process(data: Vec<u8>) {
// data is owned here, dropped at end of function
}
let v = vec![1, 2, 3];
process(v);
// v is no longer accessible — ownership moved
// SafeC: region-based
region Pool { capacity: 4096 }
void process(&arena<Pool> int data) {
// data points into Pool, cannot escape
}
&arena<Pool> int v = new<Pool> int;
*v = 42;
process(v);
// v is still accessible — no ownership transfer
References and plain values aren't owned. Values of a type that owns a resource — one with a drop() method, such as Vec<T> or String — are: they move like Rust's, and the compiler checks that each is dropped or moved exactly once on every path. Unlike Rust it never inserts the drop() call; you write it (or a defer), and forgetting it is a compile error. See Ownership & Drop.
No lifetime elision — because there's no lifetime parameter to elide
Rust elides lifetimes in common cases like fn first(s: &str) -> &str, inferring that the returned borrow's lifetime is tied to s's. SafeC doesn't have an equivalent to elide, because it doesn't have lifetime parameters at all — regions are a fixed, closed set (stack, heap, arena<R>, static, and the region-less &T/?&T form that opts out of tracking one), not per-call-site-inferred spans. The practical consequence is stricter than "annotations are always explicit": a &stack reference can never be returned from a function, full stop — not even the caller's own stack reference handed right back. longest's Rust signature (fn longest<'a>(a: &'a str, b: &'a str) -> &'a str) has no direct SafeC equivalent for &stack parameters, because there's no lifetime variable 'a to tie the return value to — the closest fit is a region-less &str return, which sidesteps the question by not tracking a specific lifetime at all rather than inferring one.
// Rust: the returned borrow's lifetime is tied to whichever input it came from
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
When a function genuinely needs to hand back a reference to data the caller gave it, use &arena<R> (or &static) instead of &stack — region membership, not a lifetime relationship, is what's tracked, so an arena reference can be returned, stored, and passed around freely as long as everyone agrees on the region:
region Pool { capacity: 8192 }
// &arena<Pool> can be returned — &stack cannot
&arena<Pool> int longest(&arena<Pool> int a, &arena<Pool> int b) {
if (*a > *b) { return a; }
return b;
}
This is the same trade-off Region-Based Safety Model describes: SafeC is more permissive than Rust for arena-shaped code (no borrow-checker fights over cyclic/shared arena references) and strictly more restrictive for stack references (no lifetime inference means no way to prove a returned stack reference is safe, so it's disallowed outright rather than requiring an annotation).
No borrow checker "fights"
Rust's borrow checker is powerful but can reject valid programs, particularly those involving self-referential structures, graph data structures, or arena allocation patterns. SafeC's region model is more permissive for these patterns:
// Multiple references into the same arena — no ownership conflict
region Pool { capacity: 8192 }
&arena<Pool> Node a = new<Pool> Node;
&arena<Pool> Node b = new<Pool> Node;
a.next = b; // both references coexist — arena guarantees validity
b.prev = a; // cyclic references are fine within the same arena
C ABI by default
Rust requires extern "C" and #[repr(C)] annotations for C interop. SafeC uses C ABI by default:
// Rust: explicit C interop annotations
#[repr(C)]
struct Point {
x: f64,
y: f64,
}
extern "C" {
fn c_process_point(p: *const Point);
}
// SafeC: C ABI by default, no annotations needed
struct Point {
double x;
double y;
};
extern void c_process_point(const Point* p);
Different trade-offs
| Aspect | Rust | SafeC |
|---|---|---|
| Learning curve | Steep (ownership, lifetimes, traits) | Moderate (C + region annotations) |
| Ecosystem | Large (crates.io) | Small (growing) |
| Safety model | Ownership + borrow checker | Region types + escape analysis, ownership checks for Drop types, Send/Sync |
| Runtime | Minimal (panic handler, allocator) | None (--freestanding) |
| C interop | FFI layer required | Native, zero-cost |
| Self-referential structs | Difficult (Pin, unsafe) | Natural (arena regions) |
| Async | Built-in (async/await) | Library (std/async: futures, executors, goto-based coroutine macros) |
Compared to Zig
Zig and SafeC share the philosophy of zero hidden cost and explicit control, but differ in their memory models and compile-time systems.
Allocators vs Regions
Zig passes allocators as function parameters. The caller decides where memory comes from. SafeC uses region types that encode the memory source in the type system.
// Zig: allocator as parameter
fn createBuffer(allocator: std.mem.Allocator, size: usize) ![]u8 {
return allocator.alloc(u8, size);
}
// SafeC: region encoded in type
region Audio { capacity: 65536 }
&arena<Audio> float createBuffer() {
return new<Audio> float; // region is part of the type
}
Zig's approach is more flexible (any allocator can be swapped in). SafeC's approach provides compile-time safety guarantees (the compiler verifies region lifetimes).
comptime vs consteval
Both languages emphasize compile-time computation, but with different mechanisms:
// Zig: comptime
fn fibonacci(comptime n: u32) u32 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const fib10 = fibonacci(10);
// SafeC: consteval
consteval int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const int fib10 = fibonacci(10);
static_assert(fib10 == 55, "fibonacci broken");
Zig's comptime is more general (it can operate on types, generate code). SafeC's consteval is more focused (compile-time value computation and branch elimination).
Preprocessor
Zig has no preprocessor. SafeC has a disciplined preprocessor subset that allows safe macros while rejecting dangerous patterns:
// SafeC preprocessor: safe subset
#define VERSION 3
#define BUFFER_SIZE 1024
#ifdef PLATFORM_LINUX
// platform-specific code
#endif
#pragma once // header guard
// Rejected in safe mode:
// #define SQUARE(x) ((x) * (x)) // function-like macros
Error handling
Zig uses error unions and try/catch. SafeC uses return values:
// Zig
fn readFile(path: []const u8) ![]u8 {
const file = try std.fs.openFile(path, .{});
defer file.close();
return file.readToEndAlloc(allocator, max_size);
}
// SafeC
int readFile(const &char path, &char buf, int bufLen) {
int fd = open(path, 0);
if (fd < 0) return -1;
defer close(fd);
return (int)read(fd, buf, (unsigned long)bufLen); // read() returns long — no implicit narrowing
}
When to Choose SafeC
SafeC is a strong fit when:
- You are already using C and want to add safety without rewriting in a new language
- C ABI compatibility is critical — your code links into existing C projects, kernels, or firmware
- Deterministic behavior is required — real-time audio, embedded systems, safety-critical software
- You want zero hidden cost — every allocation, every operation visible in the source
- You need bare-metal support — no runtime, no allocator, no standard library assumed
- Incremental adoption matters — rewrite one file at a time, not the entire project
SafeC may not be the best fit when:
- You need a large ecosystem of libraries (Rust's crates.io is far more mature)
- You prefer automatic memory management (destructors inserted for you, RAII)
- You want async/await as language syntax (SafeC's
std/asyncgives the same model as a library, with explicit state structs) - Your team is already productive in Rust or Zig
Summary
| C | C++ | Rust | Zig | SafeC | |
|---|---|---|---|---|---|
| Philosophy | Trust the programmer | Abstraction | Safety first | Simplicity | Safety + transparency |
| Memory | Manual | RAII | Ownership | Allocators | Regions |
| Cost model | Explicit | Hidden | Mostly explicit | Explicit | Explicit |
| C interop | Native | Native | FFI layer | Native | Native |
| Learning from C | N/A | Moderate | High | Moderate | Low |
| Safety | None | Partial | Strong | Partial | Strong |
SafeC's position: the safety of Rust, the transparency of C, the interop of neither-having-to-change.
Next Steps
- Types — Learn the SafeC type system
- Memory and Regions — Deep dive into region-based memory safety
- C Interop — FFI policy and interop patterns