Skip to content

rule: std-init-create-separation #296

@Sc3l3t0n

Description

@Sc3l3t0n

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:

  1. 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 this init.
    • deinit(self: *Self) or deinit(self: Self): A method to clean up any internal resources owned by the struct (e.g., internal heap allocations made by its init or other methods). This deinit does not deallocate the memory of self itself (as self is on the stack or embedded).
  2. 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-based init method 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 same init method 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 by self) is freed by destroy.
    • destroy(self: *Self, allocator: std.mem.Allocator): A static method that first calls deinit (if applicable and defined) to release internal resources, and then deallocates the memory of self (the struct instance itself) using the allocator.

Key Principles Encouraged by this Rule:

  • init Initializes, create Allocates: init functions (whether returning Self or taking *Self) should focus on setting up the state of memory that already exists. create functions are for the act of heap allocation, usually followed by calling an init.
  • 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:

  • init functions that ambiguously perform heap allocation of self. For instance, an init that returns !*Self and also performs the allocation of self should typically be named create.
  • deinit functions that deallocate self if self is a stack variable or if the deallocation of self is meant to be handled by a destroy method 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 });
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-linterArea - linter and lint rulesC-ruleCategory - Lint rule suggestionhelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions