Zig DISCUSSION

How do generics work in Zig when a function takes comptime T: type and returns a type?

Started by syedakalam Zig comptimegeneric typestype-returning functionsanytypering buffer
5 replies 248 views 6 participants
Latest activity · 30 Sep 2026

How do generics work in Zig when a function takes comptime T: type and returns a type?

syedakalam Zig Forum
#1

In the standard library I keep seeing things like std.ArrayList(u8) and definitions of the form fn Queue(comptime T: type) type { return struct { ... }; }. I want a fixed-capacity ring buffer for an embedded project, one instance holding u8 UART bytes and another holding 16-bit ADC samples, without any heap allocation.

How does a function that returns a type actually work, is Queue(u8) evaluated every time I write it, and when should I use anytype instead?

Community replies 5

Re: How do generics work in Zig when a function takes comptime T: type and returns a type?

#2

In Zig, types are ordinary values that exist only at compile time. A generic container is just a function that runs at compile time, receives a type and returns a new struct type: fn Ring(comptime T: type, comptime N: usize) type { return struct { items: [N]T = undefined, head: usize = 0, len: usize = 0 }; }.

You then name the result, const UartRing = Ring(u8, 64);, and create an instance with var rx = UartRing{};. By convention a function that returns a type is capitalised like a type.

Re: How do generics work in Zig when a function takes comptime T: type and returns a type?

#3

Calls with the same comptime arguments are memoised, so Ring(u8, 64) written in two files is the same type, and a value of one can be passed where the other is expected. Different arguments give unrelated types with separately generated code, much like C++ templates.

Nothing is allocated on a heap. Ring(u8, 64) holds 64 bytes of storage plus two usize fields, and Ring(u16, 64) holds 128 bytes plus the same two fields, all inside the variable itself. On a 32-bit microcontroller that is 72 and 136 bytes.

Re: How do generics work in Zig when a function takes comptime T: type and returns a type?

#4

Inside the returned struct, declare const Self = @This(); so that methods can refer to the type being built: pub fn push(self: *Self, value: T) bool { if (self.len == N) return false; ... }. T and N are visible in every method because the struct is declared inside the function that received them.

Since N is known at compile time, an index calculation such as (self.head + self.len) % N can be reduced to a simple mask by the optimiser when N is a power of two.

Re: How do generics work in Zig when a function takes comptime T: type and returns a type?

#5

anytype is for function parameters, not for containers: fn sum(values: anytype) u32 accepts any argument, and the body is type-checked separately for each type it is called with. It is convenient for things like writers and formatting arguments, but the signature no longer tells the reader what is expected, and a wrong argument produces an error that points inside the function body.

Prefer an explicit comptime T: type parameter when the caller should choose the type, and anytype when the type should be inferred from the argument.

Re: How do generics work in Zig when a function takes comptime T: type and returns a type?

#6

There are no trait or interface declarations, so constraints are written as ordinary compile-time code. At the top of the generic function you can inspect the arguments, for example with @typeInfo(T) or @sizeOf(T), and call @compileError when they are unsuitable: if (N == 0 or (N & (N - 1)) != 0) @compileError("capacity must be a power of two");.

The check runs once per instantiation, costs nothing at run time, and gives the user of your type a clear message instead of a failure deep inside a method.

TEP COMMUNITY