-
Notifications
You must be signed in to change notification settings - Fork 13
Open
Labels
A-linterArea - linter and lint rulesArea - linter and lint rulesC-ruleCategory - Lint rule suggestionCategory - Lint rule suggestionhelp wantedExtra attention is neededExtra attention is needed
Description
Description
This rule promotes adherence to idiomatic Zig conventions for object lifecycles, emphasizing a clear separation between memory allocation and initialization. It distinguishes two primary patterns, reflecting common usage in the Zig standard library and community:
-
Stack-Oriented (or Value-Based) Lifecycle:
init(...) Self: A function (often static on the struct type or a free function) that returns an initialized struct by value. This is suitable for structs primarily intended for stack allocation or direct embedding within other structs. The struct itself is not allocated on the heap by thisinit.deinit(self: *Self)ordeinit(self: Self): A method to clean up any internal resources owned by the struct (e.g., internal heap allocations made by itsinitor other methods). Thisdeinitdoes not deallocate the memory ofselfitself (asselfis on the stack or embedded).
-
Heap-Oriented (or Pointer-Based) Lifecycle:
create(allocator: std.mem.Allocator, ...) !*Self: A static method responsible for allocating memory for the struct on the heap using an allocator. It then typically calls a pointer-basedinitmethod on the newly allocated instance.init(self: *Self, ...): A method that initializes the state of an already allocated struct, receiving a pointer to it. This sameinitmethod can also be used for initializing pre-allocated stack variables (by passing a pointer to the stack variable).deinit(self: *Self)(Optional but common): A method to clean up internal resources owned by the struct before its main memory (pointed to byself) is freed bydestroy.destroy(self: *Self, allocator: std.mem.Allocator): A static method that first callsdeinit(if applicable and defined) to release internal resources, and then deallocates the memory ofself(the struct instance itself) using the allocator.
Key Principles Encouraged by this Rule:
initInitializes,createAllocates:initfunctions (whether returningSelfor taking*Self) should focus on setting up the state of memory that already exists.createfunctions are for the act of heap allocation, usually followed by calling aninit.- Clarity of Ownership and Responsibility: These patterns make it explicit who is responsible for memory allocation and deallocation for both the main struct and any internal resources.
- Consistency with
std: Aligns with common practices in Zig's standard library, improving code readability and maintainability.
This rule would discourage:
initfunctions that ambiguously perform heap allocation ofself. For instance, aninitthat returns!*Selfand also performs the allocation ofselfshould typically be namedcreate.deinitfunctions that deallocateselfifselfis a stack variable or if the deallocation ofselfis meant to be handled by adestroymethod in heap-oriented lifecycles.
Examples
Examples of incorrect code for this rule:
const std = @import("std");
// Incorrect: `init` allocates `self` and returns a pointer, blurring 'create' and 'init'.
// `deinit` then deallocates `self`, blurring 'deinit' and 'destroy'.
const BadConvention = struct {
x: i32,
pub fn init(allocator: std.mem.Allocator, val: i32) !*BadConvention {
std.debug.print("BadConvention.init (allocating self - incorrect for 'init')\n", .{});
const self = try allocator.create(BadConvention);
self.x = val;
return self;
}
pub fn deinit(self: *BadConvention, allocator: std.mem.Allocator) void {
std.debug.print("BadConvention.deinit (deallocating self - incorrect for 'deinit')\n", .{});
allocator.destroy(self);
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); // GPA's own deinit
const allocator = gpa.allocator();
const b = try BadConvention.init(allocator, 10);
defer b.deinit(allocator);
std.debug.print("BadConvention value: {}\n", .{b.x});
}Examples of correct code for this rule:
const std = @import("std");
// --- Pattern 1: Struct with Stack-Oriented Lifecycle ---
const StackyStruct = struct {
data: i32,
// Example: internal buffer that might be heap allocated by StackyStruct itself
internal_buffer: []u8,
// `init` returns the struct by value.
// It might perform internal allocations.
pub fn init(allocator: std.mem.Allocator, value: i32) !StackyStruct {
std.debug.print("StackyStruct.init (value return)\n", .{});
const self = StackyStruct{
.data = value,
.internal_buffer = try allocator.alloc(u8, 5),
};
return self;
}
// `deinit` cleans up internal resources.
// Takes `*Self` because it modifies `internal_buffer` and `allocator_ref`.
pub fn deinit(self: *StackyStruct, allocator: std.mem.Allocator) void {
std.debug.print("StackyStruct.deinit (cleaning internal)\n", .{});
allocator.free(self.internal_buffer);
}
};
// --- Pattern 2: Struct with Heap-Oriented Lifecycle (and usable on stack via ptr init) ---
const HeapyStruct = struct {
data: u64,
// Example: internal resource
another_buffer: []u8,
// `init` takes a pointer, initializes already allocated memory.
pub fn init(self: *HeapyStruct, allocator: std.mem.Allocator, value: u64) !void {
std.debug.print("HeapyStruct.init (ptr receiver)\n", .{});
self.data = value;
self.another_buffer = try allocator.alloc(u8, 3);
}
// `deinit` for internal cleanup, called by `destroy` or directly for stack instances.
pub fn deinit(self: *HeapyStruct, allocator: std.mem.Allocator) void {
std.debug.print("HeapyStruct.deinit (cleaning internal)\n", .{});
allocator.free(self.another_buffer);
}
// `create` allocates memory for `HeapyStruct` and calls `init`.
pub fn create(allocator: std.mem.Allocator, value: u64) !*HeapyStruct {
std.debug.print("HeapyStruct.create (allocating self, then calling init)\n", .{});
const self = try allocator.create(HeapyStruct);
// Pass allocator to init if init itself needs to make further allocations for members
self.init(allocator, value) catch |err| {
allocator.destroy(self); // Cleanup on error after create
return err;
};
return self;
}
// `destroy` calls `deinit` then deallocates `self`.
pub fn destroy(self: *HeapyStruct, allocator: std.mem.Allocator) void {
std.debug.print("HeapyStruct.destroy (calling deinit, then deallocating self)\n", .{});
self.deinit(allocator); // Clean internal resources
allocator.destroy(self); // Free the main struct instance
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); // Deinit the GPA itself
const allocator = gpa.allocator();
// --- StackyStruct Example (init returns value) ---
std.debug.print("\n--- StackyStruct (Stack Lifecycle) ---\n", .{});
var stack_s = try StackyStruct.init(allocator, 100); // Pass allocator for internal use
defer stack_s.deinit(allocator);
std.debug.print("StackyStruct data: {}, internal: {s}\n", .{ stack_s.data, stack_s.internal_buffer });
// --- HeapyStruct Example (Heap Lifecycle) ---
std.debug.print("\n--- HeapyStruct (Heap Lifecycle) ---\n", .{});
var heap_h = try HeapyStruct.create(allocator, 200);
defer heap_h.destroy(allocator);
std.debug.print("HeapyStruct data: {}, internal: {s}\n", .{ heap_h.data, heap_h.another_buffer });
// --- HeapyStruct also on stack (using its pointer-based init/deinit) ---
std.debug.print("\n--- HeapyStruct (Stack Lifecycle via Pointer Init) --*\n", .{});
var stack_ptr_h: HeapyStruct = undefined;
try stack_ptr_h.init(allocator, 200);
defer stack_ptr_h.deinit(allocator);
std.debug.print("HeapyStruct data: {}, internal: {s}\n", .{ stack_ptr_h.data, stack_ptr_h.another_buffer });
}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
A-linterArea - linter and lint rulesArea - linter and lint rulesC-ruleCategory - Lint rule suggestionCategory - Lint rule suggestionhelp wantedExtra attention is neededExtra attention is needed