Generics
SafeC generics are compile-time only. Every generic function is fully monomorphized — the compiler generates a separate copy for each concrete type used. There are no vtables, no type erasure, and no runtime dispatch.
Structs and unions can be generic too — generic<T> struct Pair { T first; T second; }; — fully monomorphized per concrete type argument, the same as generic functions; see Generic Methods below for struct methods and out-of-line definitions. The standard library's own containers (Vec, HashMap, BST, ...) predate this feature and still use a void*-based struct plus generic<T> wrapper functions for type-safe access (vec_push_t<T>, map_get_t<T>, ...) rather than being genuinely generic structs themselves — see Standard Library Overview for that pattern, which remains a legitimate approach in its own right (e.g. for a container whose backing storage genuinely shouldn't care about the element type at the ABI level).
Generic Functions
Declare a generic function with generic<T> before the return type:
generic<T>
T max(T a, T b) {
if (a > b) return a;
return b;
}The compiler infers the type argument from the call site:
int m1 = max(3, 7); // instantiates max<int>
double m2 = max(1.5, 2.7); // instantiates max<double>Each instantiation produces a separate function in the generated code.
Traits and Constrained Generics
Type parameters can be constrained with traits to restrict what types are accepted:
generic<T: Numeric>
T clamp(T val, T lo, T hi) {
if (val < lo) return lo;
if (val > hi) return hi;
return val;
}The Numeric constraint ensures T supports arithmetic and comparison. The compiler rejects instantiation with types that don't satisfy the constraint.
Declaring a Trait
A trait is a named set of method signatures:
trait Drawable {
void draw() const;
}Traits in SafeC are structural ("duck-typed"), not nominal — there's no impl Trait for Type block. A struct satisfies a trait automatically as soon as it defines methods with matching names and signatures:
struct Circle {
double radius;
void draw() const; // satisfies Drawable — nothing else needed
}
void Circle::draw() const { printf("circle r=%.1f\n", self.radius); }
generic<T: Drawable> void render(T shape) { shape.draw(); }
render(circle); // OK: Circle has a matching draw() constConformance is checked at the call site during monomorphization: if the concrete type substituted for T doesn't have every method the trait bound requires, instantiation fails with a compile error there — not at the (possibly far-away, library-internal) point where the generic function's body uses that method.
Built-in Traits
| Trait | Required Operations |
|---|---|
Numeric | Arithmetic (+, -, *, /) |
Eq | ==, != |
Ord | <, >, <=, >= |
Add | + only |
Sub | - only |
Mul | * only |
Div | / only |
Two further traits are satisfied structurally by the type system itself rather than by user-defined methods: Indexed (array, slice, and vec<T,N> types — anything usable with []) and Pointer (raw pointer and reference types).
Variadic Generics
SafeC supports variadic type packs with generic<T...>. A variadic generic parameter accepts any number of arguments of any types, and sizeof...(T) gives the pack's size:
generic<T...>
unsigned long count_args(T... args) {
return sizeof...(T);
}
unsigned long n = count_args(1, 2.0, 'a'); // n is 3A pack forwards into another call by naming it as a bare argument — args, not args... — which expands to the pack's actual arguments at each monomorphized call site:
int sum3(int a, int b, int c) { return a + b + c; }
generic<T...>
int forward_sum(T... args) {
return sum3(args); // expands to sum3(args0, args1, args2) for a 3-element pack
}
int n = forward_sum(1, 2, 3); // n == 6A pack element can also be indexed with a literal (constant) index — args[0], args[1], ... — resolved at monomorphization time to the corresponding argument:
generic<T...>
int first_int(T... args) {
return args[0];
}Literal indices only
args[i] requires i to be a compile-time integer literal, not a runtime variable — the pack doesn't exist as a real indexable array at codegen time, each args[N] is rewritten to the Nth actual argument during monomorphization. Looping over a pack with a runtime index isn't supported; write sizeof...(T) recursion or repeat the literal-index form for each position you need instead.
Monomorphization
When the compiler encounters a call to a generic function, it:
- Infers the type argument(s) from the call-site argument types
- Deep-clones the function AST, substituting type parameters with concrete types
- Mangles the name:
max<int>becomes__safec_max_int - Analyzes the monomorphized copy through the full semantic analysis pipeline
- Emits code for each distinct instantiation
Generic function bodies are skipped during the first semantic analysis pass — they are only type-checked after monomorphization with concrete types.
Name Mangling
Monomorphized functions follow the pattern __safec_<name>_<type>:
| SafeC | Mangled Name |
|---|---|
max<int> | __safec_max_int |
max<double> | __safec_max_double |
Pair<int> | __safec_Pair_int |
Code Size
Each unique instantiation generates separate code. If you instantiate max with 5 different types, 5 separate functions are emitted. This trades binary size for runtime performance (no indirection).
Generic Structs and Methods
A struct (or union) can carry its own type parameters, declared the same way as a generic function's:
generic<T>
struct Container {
T value;
int count;
T get() const;
void set(T new_value);
};
struct Container<int> c;
c.set(42);
int v = c.get(); // v == 42T get() const;/void set(T new_value); above are in-body method declarations — write the bodies out-of-line the same way you would for a non-generic struct's methods, just with a generic<T> line above each definition too. The out-of-line qualifier is the plain struct name (Container::get()), not Container<T>::get() — the type parameter is already in scope from the generic<T> line, so it isn't repeated on the method-owner name:
generic<T>
T Container::get() const {
return self.value;
}
generic<T>
void Container::set(T new_value) {
self.value = new_value;
}Interaction with Regions
Generic types can be combined with region-qualified references:
generic<T>
?T find_in_slice([]T haystack, T needle) {
for (int i = 0; i < haystack.len; i++) {
if (haystack[i] == needle) return some(haystack[i]);
}
return none;
}Region qualifiers on references inside generic code follow the same rules as non-generic code. The compiler checks region safety after monomorphization.
Limitations
- No runtime dispatch: generics are always monomorphized. There are no trait objects or dynamic dispatch.
- No partial specialization: you cannot provide a specialized implementation for a subset of types.
- No default type arguments: every type parameter must be inferred or explicitly provided.
- Inference is call-site only: the compiler infers
Tfrom function arguments. It does not infer from the return type. - Inference doesn't unify a
T*/[]Tparameter against a bare fixed-size array argument — pass an already pointer/slice-typed value instead. See Functional Programming for a worked example of this exact case.
For everything else in SafeC's "no runtime dispatch" story — traits, the vtable-free fn_eval primitive, and how to build genuine heterogeneous runtime dispatch explicitly when generics alone can't — see Polymorphism & OOP.