Choosing a Zero‑Cost Generic Vector in Zig Using Compile‑Time Parameters
A decision guide for selecting a zero‑cost generic vector in Zig, comparing comptime struct fields, anytype parameters, and opaque erasure, with a concrete implementation and verification steps.
26 Aug 2025, 01:36 UTC

Decision and Constraints
You need a generic vector type that works on all Zig targets, adds no runtime overhead, and gives full type safety. The element type must be known when the vector is instantiated because the code is performance‑critical and cannot afford indirection or runtime type checks.
Option Comparison
| Option | Mechanism | Pros | Cons |
|---|---|---|---|
| Comptime struct fields | Parametrize the struct with a comptime type T | Zero overhead, full type safety, idiomatic Zig | Requires T to be comptime‑known; compile time grows with each distinct T |
| Anytype parameters | Pass @TypeOf() as a comptime parameter | Works with any type, still zero‑cost, flexible for heterogeneous containers | Slightly more verbose; still limited to comptime‑known parameters |
| Void* erasure | Store elements as *opaque and cast on access | Can handle runtime‑known types; avoids compile‑time cost | Loses safety, needs manual alignment and bounds checks, incurs runtime cost |
Trade‑Off Summary
Comptime struct fields give the best performance and safety but increase compile‑time complexity and demand that the element type be known at compile time. Anytype parameters provide a middle ground with similar performance and a bit more boilerplate. Void* erasure avoids compile‑time cost but sacrifices safety and introduces runtime overhead, making it unsuitable for the stated performance‑critical scenario.
Concrete Implementation: Comptime Struct Fields
Below is a minimal generic vector using comptime struct fields. It omits resizing for clarity; a production version would add grow logic.
const std = @import("std");
const Allocator = std.mem.Allocator;
pub fn Vector(comptime T: type) type {
return struct {
data: []T,
len: usize,
capacity: usize,
pub fn init(allocator: Allocator, capacity: usize) !Self {
var self: Self = undefined;
self.data = try allocator.alloc(T, capacity);
self.len = 0;
self.capacity = capacity;
return self;
}
pub fn deinit(self: *Self, allocator: Allocator) void {
allocator.free(self.data);
}
pub fn append(self: *Self, value: T) void {
if self.len >= self.capacity {
// In a real implementation you would grow the buffer here.
return;
}
self.data[self.len] = value;
self.len += 1;
}
pub fn get(self: *Self, index: usize) T {
return self.data[index];
}
};
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
const IntVec = Vector(i32);
var vec = try IntVec.init(allocator, 4);
defer vec.deinit(allocator);
vec.append(10);
vec.append(20);
std.debug.print("Vec: {}\n", .{vec});
// Example with a custom struct
const Point = struct { x: f32, y: f32 };
const PointVec = Vector(Point);
var pvec = try PointVec.init(allocator, 2);
defer pvec.deinit(allocator);
pvec.append(Point{ .x = 1.0, .y = 2.0 });
pvec.append(Point{ .x = 3.0, .y = 4.0 });
std.debug.print("First point: {}\n", .{pvec.get(0)});
}
Verification Steps
- Save the code to a file, e.g.,
vector_test.zig. - Build with release‑safe mode to enable safety checks:
zig build-exe vector_test.zig -Drelease-safe - Run the executable:
Expected output (formatting may vary):./vector_testVec: { .data = [10, 20, 0, 0], .len = 2, .capacity = 4 } First point: Point{ .x = 1.0, .y = 2.0 } - To confirm compile‑time type safety, modify the
mainfunction to attempt appending a mismatched type, for example:
Re‑run the build step. The compiler should emit an error similar to:vec.append(3.14); // f64 to Vector(i32)
This demonstrates that type mismatches are caught at compile time, with no runtime penalty.error: cannot cast 3.141592653589793 to i32
Limitations and Practical Checks
- The example does not implement buffer growth; a real vector would need to reallocate and copy when
len == capacity. Adding this logic does not affect the comptime generic approach. - Compile time increases with each distinct
Tused. Monitor build times if you instantiate the vector with many types. - Verify the generated assembly for a given
T(e.g.,zig build-exe -femit-asm=vector_test.asm vector_test.zig) to confirm that the code contains only direct loads/stores and no type‑dispatch overhead.
By following this guide you can decide that comptime struct fields are the appropriate choice for a zero‑cost, type‑safe generic vector in Zig, implement it as shown, and validate correctness with the provided build and run steps.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.