I wonder: Maybe the distinction between deinit(self: Self) and deinit(self: *Self) depends on whether during the procedure of “deinitializing”, it is helpful to temporarily modify the data structure throughout the process?
But isn’t that an implementation detail that shouldn’t be reflected in the interface visible to the caller of the function? And then again, it has implications for self.* = undefined (which might be unnecessary anyway?).
Maybe I’m overthinking, but as I read earlier, Zig’s philosophy (Zen) suggests there should be:
- Only one obvious way to do things.
So it would be helpful to have some sort of rule or indication when to use which for deinit (and other functions that invalidate a data structure).
Now the following is just a thought-experiment / brainstorming:
Would it be (in theory) justified to have a language feature for arguments that are deinitialized? Something like deinit(deinitialize x: Self). The compiler would then know that the passed argument will be considered unusable after the function returned. Or, from the point of view of the caller, already be unusable right after the function is called (i.e. when it starts executing). The compiler could then decide on its own whether to pass the argument by value or by reference. But perhaps we have something like this in the language already? What about:
pub fn deinit(noalias x: *Self) void {
// …
x.* = undefined;
}
Due to the noalias keyword, the caller would have to guarantee to not use the memory from the moment when the deinit function is called (which seems reasonable), and the compiler could then, in the spirit of Parameter Reference Optimization (PRO), decide on its own whether it will pass self by value or by reference.
Currently, this doesn’t seem idiomatic at all though. But I’m curious if my thoughts are just nonsense, or if they make sense to some extent?
Also, these hypothetical considerations aside, it still would be helpful to have some rule-of-thumb at hand, which way to go. (Maybe such advice could change in future though, e.g. due to #1108).
Update:
I did some more research, and found two issues on GitHub in that matter:
- Proposal #6322: Convention, deinit doesn’t invalidate (closed/rejected in November 2021).
- This proposal suggested that "by convention,
deinitnever invalidates memory and always takesselfas a value, i.e. the opposite of what I proposed/assumed above.
- This proposal suggested that "by convention,
- Issue #9814: Constness inconsistencies in the standard library (still open).
- Here, inconsistencies in Zig’s standard library are brought up, specifically regarding functions that invalidate values.
- As of January 2022, @andrewrk states that he is “willing to walk back the current convention, which is leaning towards mutable self pointer for deinit.” (I assume this means that there was a leaning towards using
self: *Selffor deinitialization, but openness to shift towards usingself: Selfinstead.)
However, if I understand right, turning down PRO happened about two and a half years later, which leaves me wondering if shifting towards self: Self is still a good idea.
So it looks like the question is unresolved yet. I hope any inconsistencies in std will get resolved, and that there will be an easy rule I can follow as a programmer when creating my own deinit functions.
I personally feel like “noalias self: *Self” is the right solution, because it allows the implementer of deinit to modify the value during the process of invalidation, which might come in handy (and, as a bonus, be able to set it to undefined in the end).
But I would really like to hear other people’s opinion on that, and maybe arguments/implications that I’m overlooking.
P.S.: I think requiring a mutable pointer is also semantically correct because it forces the user to use var rather than const, which is (in my opinion) correct, as invalidation is some sort of mutation the programmer should be aware of.