Why is memory still leaked after calling `.toOwnedSlice` on an ArrayList

The docs for .toOwnedSlice readThe caller owns the returned memory. Empties this ArrayList, Its capacity is cleared, making deinit() safe but unnecessary to call.”. However, running this test:

const std = @import("std");
const print = std.debug.print;

test ".toOwnedSlice does not seem to make deinit unnecessary" {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var listOfLists = std.ArrayList([]u32).init(allocator);
    try listOfLists.append(try buildAList(0, allocator));
    try listOfLists.append(try buildAList(1, allocator));

    print("{any}\n", .{listOfLists.toOwnedSlice()});
}

fn buildAList(val: u32, allocator: std.mem.Allocator) ![]u32 {
    var list = std.ArrayList(u32).init(allocator);

    try list.append(val);

    return list.toOwnedSlice();
}

shows three memory leaks - two at try list.append(val);, and one at try listOfLists.append(try buildAList(0, allocator));, contrary to my expectations.

Unfortunately I don’t have enough Zig terminology to articulate my question any better than this, but hopefully my confusion is clear. What’s going on here that I’m misunderstanding?

Hmnm - this has no memory leaks:

const std = @import("std");
const print = std.debug.print;

test ".toOwnedSlice does not seem to make deinit unnecessary" {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var list_of_lists = std.ArrayList([]u32).init(allocator);
    try list_of_lists.append(try buildAList(0, allocator));
    try list_of_lists.append(try buildAList(1, allocator));

    const outer_slice = try list_of_lists.toOwnedSlice();
    print("{any}\n", .{outer_slice});
    for (outer_slice) |inner_slice| {
        allocator.free(inner_slice);
    }
    allocator.free(outer_slice);
}

fn buildAList(val: u32, allocator: std.mem.Allocator) ![]u32 {
    var list = std.ArrayList(u32).init(allocator);

    try list.append(val);

    return list.toOwnedSlice();
}

So, is it accurate to say that .toOwnedSlice() means that you don’t need to call .deinit() on the ArrayList, but you still need to call allocator.free on the resultant slice?

Yes - that is precisely what “caller owns the returned memory” means.

3 Likes

Got it - thanks!