Usage of `self.* = undefined` in deinit

I keep pondering about where to gravitate, so I would like to get some thoughts on mutable vs. const deinit.

I’ve noticed that common patterns are:

pub fn deinit(self: Foo, allocator: Allocator) void {
    allocator.free(self.something);
}

or

pub fn deinit(self: *Foo, allocator: Allocator) void {
    allocator.free(self.something);
    self.* = undefined;
}

I suppose the purpose of the undefined is to help with debugging, since in debug, undefined sets the memory to particular value.

That’s nice.

(Maybe self.* = .empty, could be safer in some circumstances, but that’s besides the point; I only care about mutability here.)

However, using this form means that the value holding this struct must be mutable, so if I’ve created the memory using const foo = Foo.init() then I’ll have to change it to var foo = Foo.init() and it feels weird to do it just because of the .deinit().

It becomes even worse if Foo is nested few types deeper.

So where do you lean?

1 Like

I agree with const foo = Foo.init(). The reasons are as follows: we often combine scope and defer to achieve resource release. From this perspective, setting foo to undefined doesn’t make much sense, because it will be leaving the scope soon after it is released.

2 Likes

Generally, I’ve used defer foo.* = undefined at the start of my deinit functions.
I don’t think it’s common for me to have a foo stored as a constant and have foo need to release resources in a deinit at the same time. Needing this might be a sign that your allocations are too fine grained.

6 Likes

It’s only worth setting it to undefined if you already need to take a mutable pointer.

1 Like

I think system resource handles, generational indices, and opaque pointers are common use cases.

2 Likes

Can you elaborate, please?

Sure, though it’s kind of hard to give a full explanation without digging into a lot of related concepts. Trying to keep it short:

The somewhat “ideal” zig memory management / data structure looks like a single layer of a struct containing several different ArrayLists (or MultiArrayList, or whatever). There isn’t much depth, or individually allocated objects. So that top level struct might have a deinit method, but it definitely wouldn’t be constant since those lists would need to change many times during the object’s use.
Andrew talks more about this here: Programming without pointers

Failing a structure like that, you’re ideally doing batch processing that lets you put your allocations in an arena, which would be freed all at once without any individual deinit method.

For a struct to both be constant and also have a resource its managing implies code built in a way that’s more ergonomic in a language with garbage collection or RAII. This isn’t definitely wrong, nor is it always ideal to avoid. However this feels like a question that would come up because you’re managing things at an individual level too much. That’s why I bring it up, but also hedge it with an italicized ‘might’.

If you, eg, have a generational index into an ArrayList, then your “release this handle” method probably isn’t touching the handle, but it is probably touching the slot the handle points to. That slot should probably be set to undefined in that case (unless it’s set to a value related to tracking that it is free).

4 Likes

FWIW (I have no idea if this is idiomatic), my deinit methods always take a *Self pointer. Although they don’t always need to, I do it for consistency and in case I may want to set them to undefined in the future.

At first I tried passing by value when transferring ownership (which is what is happening in a deinit call), because this is what I was used to doing in Rust. But I don’t think this applies in Zig, because params are const. (In Rust a param can be mut.) I really didn’t like having to reassign params to a var in order to modify a value I now own, especially since shadowing isn’t allowed.

So now I always pass a non-const pointer when transferring ownership, meaning that the callee often makes a copy. I like that because the copy is explicit. And I only pass by value when the callee is meant to treat the value as immutable. This all took some time to feel comfortable, but it does feel very consistent now.

But yes, this means that I need to use var more often. I don’t mind that.

1 Like

Thank you for the writeup!

I’m still new to non-GC so I have a ton to learn. The talk you linked is great. (As usual with Andrew’s talks, I ended up pausing all the time to looking at the code and learning much more than he probably intended.)

Incidentally, just yesterday I spent many hours figuring out (and experimenting and flip-flopping, as I tend to do) how to design memory ownership model in my library. I can see now that keeping my memory model flat I could have avoided most of it. I actually ended up going just a little bit in that direction and even that helped, although in my case it was more like “a broke clock being right twice a day” :laughing:.

1 Like