How to call a declaration (fn) inside a struct (type) by its name?

Hey everyone! I’m trying to implement a simple helper function that recursively calls deinit() on whatever comes inside. I come up with the following pseudocode that, I hope, will convey my intent:

const std = @import("std");

const Item = struct {
    item: u8,
    fn deinit() void {
        std.debug.print("Invoked inside Item.\n", .{});
    }
};

const Items = struct {
    foos: []Item,
    bars: []Item,
    fn deinit() void {
        std.debug.print("Invoked inside Items.\n", .{});
    }
};

fn deinitDeep(comptime elm: type) void {
    if (@hasDecl(elm, "deinit")) {
        elm.deinit();
    }
    // for (@fieldsOf(elm)) |field| {
    // deinitDeep(field);
    // }
}

pub fn main() !void {
    var fooItems = [_]Item{ Item{ .item = 1 }, Item{ .item = 2 } };
    var barItems = [_]Item{ Item{ .item = 3 }, Item{ .item = 4 } };
    var end: u8 = 2;
    const things: Items = .{ .foos = fooItems[0..end], .bars = barItems[0..end] };
    deinitDeep(@TypeOf(things));
}

I tried googling how to call something by name in Zig but it didn’t work out :slight_smile: So I need some help. I’m very new to Zig, so please do not make any assumptions :blush:

Update:

Solved. Thanks everyone in the thread. Here is the final code tailored for use on nested std.ArrayList’s:

const std = @import("std");

fn deinitDeep(ptr: anytype) void {
    if (@typeInfo(@TypeOf(ptr)) != .Pointer) return;

    const val_T = @TypeOf(ptr.*);
    if (@typeInfo(val_T) == .Struct and @hasDecl(val_T, "deinit")) {
        if (@hasField(val_T, "items")) {
            for (ptr.items) |*item_ptr| {
                if (@typeInfo(std.meta.Child(@TypeOf(item_ptr))) != .Struct) break;
                deinitDeep(item_ptr);
            }
        }
        ptr.deinit();
    }
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const alloc = gpa.allocator();

    var line = std.ArrayList(u8).init(alloc);
    try line.append(42);

    var lines = std.ArrayList(std.ArrayList(u8)).init(alloc);
    try lines.append(line);

    deinitDeep(&lines);
}

Hey @timfayz , welcome to Ziggit. I haven’t tried it, but couldn’t you just call the function directly once you’ve asserted the decl exists?

if (@hasDecl(elm, "deinit")) elem.deinit();
1 Like

Thanks @dude_the_builder! You solved half of the problem. I updated the post with the example that works. Now I need to figure out the field iteration part.

1 Like

Here’s a way to write it so it’s possible to mutate an instance inside deinit():

const std = @import("std");

const Thing = struct {
    item: u8,

    fn deinit(self: *Thing) void {
        _ = self;
    }
};

const Things = struct {
    items: []Thing,

    fn deinit(self: *Things) void {
        _ = self;
    }
};

fn deinitDeep(something: anytype) void {
    const SomeType = std.meta.Child(@TypeOf(something));
    if (@hasDecl(SomeType, "deinit")) {
        something.deinit();
    }

    if (@hasField(SomeType, "items")) {
        for (something.items) |*item| {
            deinitDeep(item);
        }
    }
}

test deinitDeep {
    var items = [_]Thing{ .{ .item = 1 }, .{ .item = 2 } };
    var things = Things{ .items = items[0..] };
    deinitDeep(&things);
}
1 Like

Are you sure this is better than manually writing a deinit function?
I think using an automatic deinit function might cause you harm in the future:
It can’t capture nuances, like what if your recursive deinit involves a library struct, that internally already recursively deinits its elements? Then you’ll have a double free. And you won’t even know what’s going on.

I think it would be better to learn writing your deinit functions properly, it’s really not that hard:

    fn deinit(self: Items) void {
        for(self.foos) |foo| {
            foo.deinit();
        }
        allocator.free(self.foos);
        for(self.bars) |foo| {
            foo.deinit();
        }
        allocator.free(self.bars);
        std.debug.print("Invoked inside Items.\n", .{});
    }

Also if you are afraid to forget freeing something: The general purpose allocator of Zig can perform leak checks which give the stacktrace where you allocated it. That makes it super easy to find missing cases in your deinitialization.

4 Likes

Hm… This is actually something I wanna do. What does the std.meta.Child(@TypeOf(something)) line?

Yeah, I understand. But the thing is I’m working with nested std.ArrayList’s and passing those nested lists from a function to a function directly. At some point a callee should deinit() them (which is me) and I really tired to iterate over children (items) to deinit() them and deinit the parent as well. At the same time, I don’t wanna create fancy wrappers with my own deinit() func that will do it for me. I really like passing structures directly, it decreases my cognitive load. So the only way I end up with is to tailor some helper function like the one @tensorush provided.

3 Likes

Returns the child type of a given type, in this case it gets the type behind the pointer (see the source). Also, check out ZLS to have Go to Definition for easily navigating Zig code.

To be honest, I looked it up when you first posted but I didn’t get what “child” refers to.

Update:

Ok, I think it’s getting late where I am. It was clearly written in source which type has a “child type”:

/// Given a parameterized type (array, vector, pointer, optional), returns the "child type".
pub fn Child(comptime T: type) type {
    ...
1 Like

Yeah, so we have to use std.meta.Child here because we’ll be passing a pointer to a struct instance (so we can mutate it in deinit) as the anytype argument to deinitDeep. So in order to get the type behind the pointer (Things) and not the pointer type itself (*Things) we use std.meta.Child.

2 Likes