Zig Ownership Patterns
Ownership is the rule that one value is responsible for freeing each allocation. The owner frees its memory when it is done, and no one else frees it.
One struct, one deinit
The struct owns one slice, and its deinit frees that slice.
const std = @import("std");
const Owned = struct {
data: []u8,
alloc: std.mem.Allocator,
pub fn deinit(self: *Owned) void {
self.alloc.free(self.data);
}
};
pub fn main() !void {
const out = std.io.getStdOut().writer();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var o = Owned{
.data = try alloc.alloc(u8, 5),
.alloc = alloc,
};
defer o.deinit();
o.data[0] = 65;
o.data[1] = 66;
try out.print("{s}\n", .{o.data[0..2]});
}
Deinit frees every field
The struct owns two slices, and its deinit frees both.
const std = @import("std");
const Pair = struct {
first: []u8,
second: []u8,
alloc: std.mem.Allocator,
pub fn deinit(self: *Pair) void {
self.alloc.free(self.first);
self.alloc.free(self.second);
}
};
pub fn main() !void {
const out = std.io.getStdOut().writer();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var p = Pair{
.first = try alloc.alloc(u8, 2),
.second = try alloc.alloc(u8, 3),
.alloc = alloc,
};
defer p.deinit();
p.first[0] = 72;
p.first[1] = 105;
p.second[0] = 66;
p.second[1] = 121;
p.second[2] = 101;
try out.print("{s} {s}\n", .{ p.first, p.second });
}
A function hands over ownership
The helper builds the greeting and returns it, and the caller becomes the owner.
const std = @import("std");
const Owned = struct {
data: []u8,
alloc: std.mem.Allocator,
pub fn deinit(self: *Owned) void {
self.alloc.free(self.data);
}
};
fn makeGreeting(alloc: std.mem.Allocator, name: []const u8) !Owned {
return Owned{
.data = try std.fmt.allocPrint(alloc, "Hello, {s}!", .{name}),
.alloc = alloc,
};
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var g = try makeGreeting(alloc, "Zig");
defer g.deinit();
try out.print("{s}\n", .{g.data});
}
Example
const std = @import("std");
const Document = struct {
title: []u8,
body: []u8,
alloc: std.mem.Allocator,
pub fn deinit(self: *Document) void {
self.alloc.free(self.title);
self.alloc.free(self.body);
}
};
pub fn main() !void {
const out = std.io.getStdOut().writer();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var doc = Document{
.title = try alloc.dupe(u8, "Guide"),
.body = try alloc.dupe(u8, "Read me"),
.alloc = alloc,
};
defer doc.deinit();
try out.print("{s}: {s}\n", .{ doc.title, doc.body });
}
Key points
- Owner: an owner is the value responsible for freeing an allocation.
- A deinit function is a cleanup function that frees everything its value owns.
- The owner is usually a struct, and deinit is defined on that struct.
- The caller pairs creation with a deferred deinit, so cleanup runs even if later code fails.
- Moving ownership means handing the value to a caller, and the caller becomes the new owner.
- A value must have exactly one owner, or its memory may be freed twice.
- Splitting ownership across two values is a bug, so the pattern keeps one clear owner.
The document owns its title and body. Main pairs creation with a deferred deinit. When main ends, deinit frees both slices, so nothing leaks.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Who frees the memory owned by a value?