Deiniting allocator passed to a function

I can declare a global general purpose allocator like:

var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
const allocator = gpa.allocator();

and use it in a function to create an arraylist, perform a few operations, and return finally return the slice from it using toOwnedSlice.

However, when I change the function signature to additionally accept an allocator as an argument, the program doesn’t work.

I’m passing a testing.allocator from a decltest and seeing memory leaks even when I defer the de-initialization of the arraylist.

I’m using zig version 0.14.0-dev.3237+ddff1fa4c on Linux x86_64.

This is the program that I’m running zig test on:

const std = @import("std");
const testing = std.testing;

// var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
// const alloc = gpa.allocator();

fn findAnagrams(alloc: std.mem.Allocator, s: []const u8, p: []const u8) ![]u8 {
    if (s.len < p.len) {
        return error.AnagramsCantForm;
    }

    var result: std.ArrayList(u8) = .init(alloc);
    // defer result.deinit();

    var freq_p: [26]u8 = @splat(0);
    var window: [26]u8 = @splat(0);

    for (0..p.len) |i| {
        freq_p[p[i] - 'a'] += 1;
        window[s[i] - 'a'] += 1;
    }

    if (std.mem.eql(u8, &freq_p, &window)) {
        try result.append(0);
    }

    for (p.len..s.len) |i| {
        window[s[i - p.len] - 'a'] -= 1;
        window[s[i] - 'a'] += 1;

        if (std.mem.eql(u8, &freq_p, &window)) {
            try result.append(@intCast(i - p.len + 1));
        }
    }

    return result.toOwnedSlice();
}

test findAnagrams {
    const test_cases = [_]struct {
        description: []const u8,
        input1: []const u8,
        input2: []const u8,
        expected: []const u8,
    }{
        .{
            .description = "substrings of p are present in s, and start at indices 0 and 6",
            .input1 = "cbaebabacd",
            .input2 = "abc",
            .expected = &[_]u8{ 0, 6 },
        },
        .{
            .description = "substrings of p start at indices 0, 1, and 2 in s",
            .input1 = "abab",
            .input2 = "ab",
            .expected = &[_]u8{ 0, 1, 2 },
        },
    };

    for (test_cases) |tc| {
        const got = try findAnagrams(testing.allocator, tc.input1, tc.input2);
        const want = tc.expected;
        std.debug.print("{s}\n", .{tc.description});
        try testing.expectEqualSlices(u8, want, got);
    }
}
1 Like

Yeah, this concept tripped me up the first time I encountered it too. The short answer is you need to free the slice you returned here:

const got = try findAnagrams(testing.allocator, tc.input1, tc.input2);
defer testing.allocator.free(got);

The reason why is when you call toOwnedSlice, you are now responsible for cleaning up the memory instead of using ArrayList’s deinit. A more detailed explanation can be found here.

4 Likes

Thanks Christian !