Skip to content

Generic Structs

A generic struct is a function that returns a type.

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
10

The declaration:

fn Stack(comptime T: type) type

means:

  • Stack is 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: []T

If T is i32, this becomes:

items: []i32

The 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) void

Without @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]T

depends 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.