#Idiomatic usage of defer

1 messages · Page 1 of 1 (latest)

arctic girder
#

Hey everyone, I am a new zig user coming to learn the language for embedded development. I used this year's advent of code to get myself started, and I have a question about the right way to free memory with defer:

// let's say i have an array of arraylists and i want to both initialize them
// in a loop while also making sure the memory is cleared when it's no longer
// needed
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
var stack_array: [9]std.ArrayList(u8) = undefined;
    var index: usize = 0;
    while (index <= 8) : (index += 1) {
        stack_array[index] = std.ArrayList(u8).init(gpa.allocator());
        defer stack_array[index].deinit();      //<- doesn't this clear memory
    }                                           //<- here?

After we have left the scope of the while, I can do things with my array of arraylists "stack_array" even though the deinit() should have happened at the end of the while. I am not sure why this is. Perhaps is it because stack_array was declared outside of the while? Where would the right place to put deinit() be in this case?

final niche
#

If I understand you correctly you want to clear all arraylists at the end of the function scope.
You can do that by defering an entire loop over stack_array:

defer for(stack_array) |list| {
    list.deinit();
}

(I've used a for loop here so we don't have to bother with the indexing)

arctic girder
#

Oh that's fascinating, you can defer a whole loop? Thanks for the tip, I will keep that in my back pocket for sure!

final niche
#

You can even defer an entire block:

defer {
    ...
}
lean dirge
#

In Zig, blocks, loops, ifs, etc. are all expressions, and can be used as such. They can even be set up to have return values.

#

This opens up all sorts of opportunities!

#

I know this is outside the scope of this question, but here's one pattern I like to use:

const arr = makeArr:{
  var made: [16]u64;
  for (made) |*elem| {
    // Put something in elem.*
  }
  break :makeArr made;
};
#

It allows me to generate some data inside a var and then discard that var in favor of a more idiomatic const.

#

I always prefer const over var because I can know everything about the former just by looking at its declaration, but some values can't be generated without using var. This pattern allows me to have both.

gray vine
#

const doesn't make much sense with an array of ArrayList though

lean dirge
#

*Assuming that I don't actually need to change the value later on. Not basing this on what the asker is doing. I just think it's a nice showcase of what can be done with these language features.

arctic girder
#

This discussion has been really interesting! Though I have arrived back to my prior question which I guess is actually about scope. Why does my defer deinit() above (copied here again for convenience) not free what's in stack_array at the end each while loop?

#
// in a loop while also making sure the memory is cleared when it's no longer
// needed
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
var stack_array: [9]std.ArrayList(u8) = undefined;
    var index: usize = 0;
    while (index <= 8) : (index += 1) {
        stack_array[index] = std.ArrayList(u8).init(gpa.allocator());
        defer stack_array[index].deinit();      //<- doesn't this clear memory
    }                                           //<- here?```
gray vine
#

i guess deinit is a noop when the ArrayList is empty?

#

i haven't checked that

#

but it would make sense, nothing has been allocated yet

gaunt kite
#

defer runs at the end of the block

#

as opposed to Go, where it runs at the end of the function

#

in your example it's running immediately as the while block ends

#

not at any point after

#

so the idea would be to initialise all the arraylists, and then deinitialise them in the for loop, at the end of the block where you actually mutated them

#

e.g., I'd re-formulate that code as

var stack_array: [9]std.ArrayList(u8) = .{ std.ArrayList(u8).init(gpa.allocator()) } ** 9;
defer for (stack_array) |*list| list.deinit();
arctic girder
#

I thought the same as well, about defer running at the end of the block. But if that's true, why does this work?

#
var stack_array: [9]std.ArrayList(u8) = undefined;
    var column: usize = 0;
    while (column <= 8) : (column += 1) {
        stack_array[column] = std.ArrayList(u8).init(gpa.allocator());
        defer stack_array[column].deinit();     //<- doesn't this clear the mem
    }                                           //<- here?
    // if memory is clear via the deinit() above, why does this work?
    try stack_array[0].append(116);            
    std.debug.print("{any}\n", .{stack_array[0].pop()});           
#

unless an arraylist doesn't require init() before append()...

gaunt kite
#

deinit just frees and clears the memory, such that it's back to how it was on init

#

All that is is a memory leak, since you don't deinit it after actually allocating memory

blazing garnet
#

(the init state of an arraylist has no memory allocated, hence why it's not failable)

arctic girder
#

ah okay got it thanks everyone!

thorny forge
#

Notice though - it may still work even if you had appended an item to each one in the loop.
list.deinit() will ask the list to free its associated memory, and nothing more.
It's up to you to not use this list after this point, unless you have reinitialized it to something.
Doing so will in all probability result in a crash -- especially in debug mode -- but this is not a given.
It may corrupt some memory in the allocator, for example, since you just told the allocator that you are no longer using that block of memory.

#

You can make it much more reliably crash when you access freed memory, by using an allocator that is specifically designed to do that -- but the simple techniques are pretty slow and unwieldly, by comparison.
That might be fine if you do like one or two very large allocations, and then only ever use parts of that blob going forward - but less so otherwise.

arctic girder
thorny forge
# arctic girder This is very interesting, thanks for the detailed reply! I am new to this so I a...

Alas, there's not really such an allocator in the stdlib - though the GPA is currently set up to cause a crash as quickly as possible - it is possible to write such an allocator.
There's not really a way to specifically catch it otherwise though - you could make and use your own pointer-esque type that always checks this stuff, or make an allocator that makes it more likely (or guarenteed) to be a segfault - but that's basically all you can do without automatic memory management.

The simplest though is to just use caution, think carefully about where things point, what can point elsewhere, and what lifetime you actually need. These things are often much easier if you understand the memory requirements of what you're trying to do already, of course.

thorny forge
graceful epoch
#

I have the same situation, but it's an array of arrays. Using QuatumDeveloper's solution, it would look like this:

defer allocator.free(words);

var i: usize = 0;
while (i < n_words) : (i += 1) {
    words[i] = try allocator.alloc(u8, n_letters_in_each_word[i]);
    // defer allocator.free(words[i]); // no, this would free when the loop ends
}
defer for (words) |word| {
    defer allocator.free(word);
}```
#

However, if the while loop works for some elements but then fails, then those elements wont be freed, right?

#

Is there any good solution?

blazing garnet
#

If you really need a truly multi-dimensional array, you can do this:

const words: [][]u8 = try allocator.alloc([]u8, n_words);
defer allocator.free(words);

for (words, n_letters_in_each_word, 0..) |*word, letters, i| {
    errdefer (words[0..i]) |w| allocator.free(w);
    word.* = try allocator.alloc(u8, letters);
}
defer for (words) |w| allocator.free(w);

However, take a moment to think about whether a flat array (just with an index calculation) would serve your purposes better. Also, even if you do need the 2D array, an alternative solution would be to allocate all the memory you'll need for the data upfront. It's slightly more verbose, but is a bit nicer on the cleanup code and also faster:

var total_size: usize = 0;
for (n_letters_in_each_word) |n| total_size += n;

const words = try allocator.alloc([]u8, n_words);
defer allocator.free(words);

const buf = try allocator.alloc(u8, total_size);
defer allocator.free(buf);

var idx: usize = 0
for (words, n_letters_in_each_word) |*word, letters| {
    word.* = buf[idx..][0..letters];
    idx += letters;
}
#

(btw, i used the new multi-for loops in both of those examples - they're pretty simple, it's just iterating over multiple arrays/slices/ranges of the same size simultaneously)

graceful epoch
#

thanks for the excellent & quick answer! I would have never thought of using errdefer like that, but it makes sense in hindsight 🙂