A generic struct is a function that returns a type.
This is one of the most important ideas in Zig. Types are values known at compile time, so a function can construct and return new types.
Here is a small stack implementation:
const std = @import("std");
fn Stack(comptime T: type) type {
return struct {
const Self = @This();
items: []T,
len: usize,
pub fn push(self: *Self, value: T) void {
self.items[self.len] = value;
self.len += 1;
}
pub fn pop(self: *Self) T {
self.len -= 1;
return self.items[self.len];
}
};
}
pub fn main() void {
var buffer: [8]i32 = undefined;
var stack = Stack(i32){
.items = buffer[0..],
.len = 0,
};
stack.push(10);
stack.push(20);
std.debug.print("{d}\n", .{stack.pop()});
std.debug.print("{d}\n", .{stack.pop()});
}The output is:
20
10The declaration:
fn Stack(comptime T: type) typemeans:
Stackis a function- the parameter is a compile-time type
- the result is another type
The returned value is a struct declaration:
return struct {
...
};Each call creates a distinct type.
For example:
Stack(i32)and:
Stack(f64)are different types.
The compiler specializes the struct using the provided type parameter.
Inside the struct, T behaves like an ordinary type name:
items: []TIf T is i32, this becomes:
items: []i32The declaration:
const Self = @This();captures the current struct type.
This allows methods to refer to the complete instantiated type:
pub fn push(self: *Self, value: T) voidWithout @This(), the method would not know the exact struct type.
A generic struct may contain compile-time logic.
This example selects storage size from a parameter:
fn Buffer(comptime T: type, comptime N: usize) type {
return struct {
data: [N]T,
};
}Usage:
const IntBuffer = Buffer(i32, 16);
const FloatBuffer = Buffer(f64, 32);The compiler generates different layouts:
[N]Tdepends entirely on the compile-time arguments.
Generic structs are commonly used for:
- containers
- allocators
- iterators
- parsers
- memory pools
- protocol implementations
Much of the Zig standard library uses this pattern.
For example, many data structures in std are generic over:
- element type
- allocator type
- hashing strategy
- context objects
A generic struct can also expose declarations conditionally.
fn Pair(comptime T: type) type {
return struct {
left: T,
right: T,
pub fn swap(self: *@This()) void {
const temp = self.left;
self.left = self.right;
self.right = temp;
}
};
}Instantiation:
var p = Pair(i32){
.left = 1,
.right = 2,
};The generated type behaves like any other struct.
There is no runtime penalty for generic code. The compiler resolves the structure and layout during compilation.
A generic struct is therefore not a container holding arbitrary values at runtime. It is a factory for producing concrete types before the program runs.
Exercise 11-5. Write a generic Point struct with fields x and y.
Exercise 11-6. Add a clear method to Stack.
Exercise 11-7. Write a generic fixed-size queue.
Exercise 11-8. Write a generic struct that stores a value and counts how many times it has been updated.