How to free (or identify) a slice-literal?

Consider the following code:

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

// Standin function for one which
// sometimes returns slice-literals (instantiated with `&.{...}` syntax),
// and sometimes slices built with an allocator
// (which can thus be `free`'d)
//
// Thanks to https://stackoverflow.com/a/77248553 for showing me the
// slice-literal approach
fn buildSlice(n: u8, allocator: std.mem.Allocator) []const u8 {
    return switch (n) {
        0 => &.{ 72, 69, 76, 76, 79 },
        else => {
            var op = std.ArrayList(u8).init(allocator);
            for (0..n) |i| {
                const value: u8 = @truncate(i);
                op.append(64 + value) catch unreachable;
            }
            return op.toOwnedSlice() catch unreachable;
        },
    };
}

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

    var slices = std.ArrayList([]const u8).init(allocator);
    for (0..7) |i| {
        const index: u8 = @truncate(i);
        slices.append(buildSlice(6 - index, allocator)) catch unreachable;
    }
    const slice_of_slices = slices.toOwnedSlice() catch unreachable;

    for (slice_of_slices) |slice| {
        std.debug.print("Received slice {s}", .{slice});
        allocator.free(slice); // Will fail on the last one
        std.debug.print(", and freed it\n", .{});
    }
}

which gives this output when run:

Received slice @ABCDE, and freed it
Received slice @ABCD, and freed it
Received slice @ABC, and freed it
Received slice @AB, and freed it
Received slice @A, and freed it
Received slice @, and freed it
Received slice HELLOBus error at address 0x106f62900
???:?:?: 0x106f61470 in ___atomic_compare_exchange_16 (???)
Unwind error at address `scratch:0x106f61470` (error.InvalidUnwindInfo), trace may be incomplete

I’m reasonably sure that the Bus error is due to the attempt to free the slice literal "HELLO" / &.{ 72, 69, 76, 76, 79 }. Fair enough - if a slice was not created with an allocator, I shouldn’t expect to be able to free it with that same allocator.

I expect that the idiomatic way to achieve what I’ve done here would be to return not a []const u8 from buildSlice, but some custom struct which exposes a .deinit method - but, for the purposes of learning, how could I implement this approach? Is there a way to do something like:

...
    for (slice_of_slices) |slice| {
        std.debug.print("Received slice {s}", .{slice});
        if (was_object_created_with_this_allocator(allocator, slice)) {
            allocator.free(slice);
        } else {
            free_slice_literal(slice)
        }
        std.debug.print(", and freed it\n", .{});
    }

or

    for (slice_of_slices) |slice| {
        std.debug.print("Received slice {s}", .{slice});
        if (!(is_slice_literal(slice)) {
            allocator.free(slice);
        } else {
            free_slice_literal(slice)
        }
        std.debug.print(", and freed it\n", .{});
    }

?

1 Like

The simplest option would just be to always return heap-allocated slices and free them all.

One option would be to keep a bit set around that tracks the indexes that need to be freed:

const std = @import("std");

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

    var slices = std.ArrayList([]const u8).init(allocator);
    // From my reading of DynamicBitSet.initEmpty, calling it with a size of 0 seems unsafe?
    // Need to look into this further, but it seems like it would call free on a pointer to stack-allocated memory.
    var heap_allocated_bit_set = try std.bit_set.DynamicBitSet.initEmpty(allocator, 1);
    defer {
        // this iterates over the set bits only
        var bit_set_it = heap_allocated_bit_set.iterator(.{});
        while (bit_set_it.next()) |i| {
            allocator.free(slices.items[i]);
        }
        slices.deinit();
        heap_allocated_bit_set.deinit();
    }

    for (0..7) |i| {
        const allocated_or_literal = try getHeapAllocatedOrLiteral(i, allocator);
        // resize but leave unset for now, we don't want to
        // free the slot if the slices.append call fails
        try heap_allocated_bit_set.resize(slices.items.len + 1, false);
        try slices.append(allocated_or_literal.slice);
        if (allocated_or_literal.is_heap_allocated) {
            heap_allocated_bit_set.set(slices.items.len - 1);
        }
    }

    // ...
}

const AllocatedOrLiteral = struct {
    slice: []const u8,
    is_heap_allocated: bool,
};

fn getHeapAllocatedOrLiteral(i: usize, allocator: std.mem.Allocator) !AllocatedOrLiteral {
    if (i % 2 == 0) {
        const slice = try allocator.dupe(u8, "heap");
        return .{
            .slice = slice,
            .is_heap_allocated = true,
        };
    } else {
        return .{
            .slice = "literal",
            .is_heap_allocated = false,
        };
    }
}

Note that this strategy would get a bit tricky when dealing with other operations than just append (remove, insert, replace, etc). You’d likely want to create a dedicated struct that has ArrayList and DynamicBitSet as fields and then have all operations that modify the list go through that instead.

1 Like

OK - does that mean replacing the &.{...} formations with:

var slice = allocator.alloc(u8, <length>);
slice[0] = '...';
slice[1] = '...';
slice[2] = '...';

? That would work, but dang it’s verbose! Is there a one-line way to create a heap-allocated slice?

Neat idea, thanks! I guess that suggestion implies that there is no way to determine from an object itself how it has been initialized/allocated (i.e. there is no allocator.allocated_this(object) function), but that it has to be determined and persisted independently? That makes sense.

var slice = try allocator.dupe(T, slice_of_T);
3 Likes
var slice = try allocator.dupe(u8, "a string literal");

Nope, that’s not part of the allocator interface.

However, you could write an Allocator implementation that keeps track of that info if you wanted to. See FailingAllocator for an example of a simple Allocator implementation that wraps another Allocator (or gimme for more examples).


Another option that I should have mentioned earlier is using an ArenaAllocator, which means that you wouldn’t have to free the slices individually at all since the memory allocated by the arena is freed all at the same time when the arena is deinited.

const std = @import("std");

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

    var arena_state = std.heap.ArenaAllocator.init(allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    var slices = std.ArrayList([]const u8).init(allocator);
    // Don't need to free the elements of slices.items individually  are heap
    // since any that allocated will have been allocated by the arena.
    defer slices.deinit();

    for (0..7) |i| {
        // Since we're passing the arena allocator here, we don't have to care about
        // whether or not we need to free the returned slice since if it's
        // heap-allocated it'll get freed when the arena is `deinit`ed.
        const allocated_or_literal = try getHeapAllocatedOrLiteral(i, arena);
        try slices.append(allocated_or_literal.slice);
    }

    // ...
}

const AllocatedOrLiteral = struct {
    slice: []const u8,
    is_heap_allocated: bool,
};

fn getHeapAllocatedOrLiteral(i: usize, allocator: std.mem.Allocator) !AllocatedOrLiteral {
    if (i % 2 == 0) {
        const slice = try allocator.dupe(u8, "heap");
        return .{
            .slice = slice,
            .is_heap_allocated = true,
        };
    } else {
        return .{
            .slice = "literal",
            .is_heap_allocated = false,
        };
    }
}
1 Like

OMG! That will be so much easier for these little Advent Of Code solutions that don’t require careful direct deinits for optimized memory usage! I don’t regret the time I spent working with the GPA as it’s taught me how to do it “the hard way” for if/when I ever need that - but this is so much easier for “one-and-done” scripts. Thank you! :pray:

1 Like
const std = @import("std");

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

    var slice = allocator.dupe(u8, &.{ 72, 69, 76, 76, 79 }) catch unreachable;
    std.debug.print("The slice is {s}\n", .{slice});
    allocator.free(slice);
}

Works like a charm, thank you! <3

1 Like

For small programs, there’ an even better solution. Leak everything and let the OS clean it up. It’ actually faster, because the OS doesn’t trust you and will double check and release every resource attached to the process anyways, so you end up doing double work.

2 Likes

Interesting suggestion - but having to scroll all the way back up past the memory leak messages just to see the output of the program (to paste in as the Advent Of Code solution) is enough of a hassle that I’d rather use the ArenaAllocator approach to avoid them. But thank you for the suggestion!

If you are letting the OS clean up then you shouldn’t call gpa.deinit() either. Then you won’t get memory leak messages.

5 Likes

The use cases for this practice are pretty narrow (academic exercises, small one-shot tools that you don’t care about their memory usage, coding speed contests). Doing this means you don’t learn anything about managing memory, and you fail to develop good practices. Sure, you can get away with it for a while, but that only works up to a point. You eventually need to learn memory management properly.

1 Like

using an arena alocator as your main alocator is perfectly fine for even somewhat complex tools
as for not de initing the allocator/freeing memory when exiting is something any software that allocates a lot/access os resources a lot should do as it will always be faster when running under an OS its just unnecessary to do it your self, and doing it your self makes exiting take longer which is bad user experience

2 Likes