2026-08-27
The asker has a Zig struct with a heap-allocated description: []const u8 field. Their init copies an input string onto the heap via an Allocator, and they want to know the idiomatic pattern for managing that memory — specifically, who owns the buffer, who frees it, and how deinit should be shaped.
Why this is interesting. Zig has no destructors, no RAII, no GC. Ownership is a documentation-and-convention discipline enforced only by whoever wrote the code. Unlike C++ (where std::string's destructor runs automatically) or Rust (where the borrow checker rejects use-after-free at compile time), Zig deliberately makes allocation and deallocation explicit. That shifts the entire lifetime question — normally decided by the language — onto the API designer.
The core design question: does MyStruct own its description, or does it borrow it? Because description is []const u8, both are structurally identical — the type doesn't tell you. The answer determines everything downstream.
Idiomatic direction. The standard-library convention is:
Allocator used at init time inside the struct itself. This way deinit doesn't require the caller to remember which allocator was used — a common source of mismatched-allocator bugs.init uses allocator.dupe(u8, input_str) to make an owned copy.deinit(self: *MyStruct) calls self.allocator.free(self.description).init with defer thing.deinit() — the closest Zig has to RAII.Gotchas.
MyStruct by value and both copies later deinit, you double-free. The convention is: don't copy owning structs; pass by pointer.deinit keeps the struct lean but shifts responsibility to the caller. Both are valid; the standard library uses both depending on the type.ArenaAllocator, calling deinit on the struct is redundant (and harmless) — the whole arena will be freed at once. This is why some Zig APIs skip individual deinit entirely for short-lived data.init. If init allocates multiple things and the second fails, you must errdefer the first's cleanup or you'll leak.