ah, and since we’re at it, might as well complete the loop…
Likewise, Zoo wouldn’t really need a destroy(), necessarily, as caller could al.destroy(myzoo) themself, but, if Zoo was more complicated, and, say, there were sub-allocations and such, then should a destroy() look like?:
...
fn destroy(self: *const Zoo, al: Allocator) void {
// ... other destructive details..., then:
self.* = undefined; // to catch use-after-free, as mentioned by @vulpesx above
al.destroy(self);
}
I have only had occasions in the past to self.* = undefined after doing various other frees; I’ve not had the pleasure of considering an al.destroy(self) … but perhaps this is where such would live? And then, as someone mentioned, this would have to be last. Is this “correct”? “Conventional”?
(If this is all clearly spelled out in a document I’m missing, please point me to it. If it’s all simply “obvious” to all newbies but me, simply kindly humor me.)
When I see `create()` I expect it to allocate on the heap. When I see `init()` I expect it to not allocate anything anywhere but to initialize the fields of an already allocated struct. It may be that `init()` allocates for filling the struct’s fields, though.
That feels like a point I could imagine some strong differing opinions (?) – I can imagine a world in which it’s taboo to allocate within a function named init(), even for filling struct fields. Of course, using normal patterns, it would be obvious to the caller because it would take an allocator. Also, the return type !T or !*T should make it quite clear. Still, it feels like there’s a lore, and @mnemnion‘s recommendation (above) seems to match the most closely to what (little yet) I’ve seen in the wild.
I’m afraid you may be looking for stronger and more specific conventions than exist for Zig, as far as I can tell. You may find individuals who have such conventions, but there won’t be agreement on them. So I suggest coming up with your own conventions that make sense to you. The strongest convention is to be consistent within a module.
So, follow-up: let’s say I’m implementing a struct and giving it a create(allocator). Say this struct almost “only” makes sense allocator-created in its natural setting; 99% of the time, this create() will be used. But there will, of course, be a Joe out there that wants to use it differently. SO, it makes sense to me that, if you provide a create(), then you also provide a destroy() (You don’t withhold the destroy and say “you shouldn’t do it that way”.) Anticipating more conversation, let’s say that an explicit destroy() (“member function”) really wouldn’t be necessary, because a caller could simply use allocator.destroy(foo) on the created object (that is to say: this object is simple enough that there would be no guts in its own destroy() function if provided). STILL, it seems to make sense to provide a destroy() if I provide a create(). This seems “right” to me – not to insist on all doing so, but to glean a likely unwritten convention. True? Any other related advice?
Finally, IF I decided I wasn’t too oddball with my own convention (where natural):
init() to return a !T (or T), always setting values
create()to return a !*T, never setting values
useful comment to user regarding the importance of calling init() after create() if uninitialized state is an invalid state
make()to return a !*T, combining create() and init() for convenience (and maybe to promote correctness) to the caller.
BUT, I think I’ll always want to provide at least separate init() and create()because, as pointed out in @matklad‘s Reserve First, it can be important for others’ initialization code to allocate, then
errdefer comptime unreachable; // End reservation phase.
… and only then perform error-impossible initialization so that state isn’t changed in an error case. With this in mind, too, I think my make() implementation would follow the same pattern, generally, calling my own create()followed by init(), at least if the creation is not very simple.
(I think I see that “Reserve First” may frequently not apply to individual struct object creation/initialization as much as it does to making modifications to an object later… so the situations would have to be considered uniquely… nevertheless.)
I think this is the stronger point, actually, and pretty characteristic with the Zig way. It seems like the signature is the main place to see what “kind” of initialization it’s about, even more so with the upcoming std.Io interface.
That said, I understand you. I also have strong instinct to keep my own code consistent across projects (since I’m often working on/off on many of them at the same time), and I hate the phase when I don’t have clarity on what those conventions are / should be (and if/why/where should I personally override them), so I end up flip-flopping and refactoring instead of making progress with the actual project.
What does it do then?
If your create function does nothing besides calling allocator.create then it is useless and you should just call allocator.create instead.
I find it very rare to write a create function at all and if I do, it is usually for things that are expected to be created in some single predefined way where the caller doesn’t have any say in what happens internally.
I think these sort of discussions quickly become tedious, I would rather leave it at:
don’t overthink it
don’t overengineer it
remove boilerplate as much as reasonable
adapt to the problem
incrementally refine (instead of overly categorizing upfront)
I think
is a step in the wrong direction, because instead of adapting to the problem you try to turn everything into the same kind of repeating boilerplate, no matter whether these create functions are needed at all.
My 2 cents:
Focus on code rather than style.
Or said another way: write code instead of talking about writing code.
The code is the problem, not the styling of the code (at least the former is the more important problem and the latter should be a natural result from refining the former, instead of forcing the code into that structure (which would create unnecessary boilerplate))
You want to end up with code that has been refined, not with code that imitates the look of other code that has been refined but it itself wasn’t actually refined.
I can empathize. I went through a period where I wanted to find “standard” conventions and felt ungrounded without them. If it helps, I suggest that a good starting point is:
Use init when a value (not a pointer to a value) is returned.
Pair deinit withinit only when something needs to be done at deinit time (obviously).
Use create and destroy instead of init and deinit, at first anyway, only if you always want the caller to have a pointer to the value, never just the value itself. This is rare as @Sze said. (For me it is so rare I never do it.)
Try to do a project, and you can see if other cases come up and what conventions you want to follow.
It does happen, not a lot of point in stack-allocating linked lists for example. But yes, rare.
I wanted to add one thing:
If the type can be used with nothing provided, in other words, there’s a good starting value for every field, just make a declaration literal: .empty or .default are nice, unsurprising names for it.
Also: read a lot of code. Trying to guess, or build intellectual abstractions about what makes good Zig, is admittedly a fun pastime, we’re all here after all. But it’s not a great way to get good at Zig. Write code, but also, read code.
All of this stuff would make a lot more sense to you @jmctagger if you had picked it up by reading code, which is how most of us did it.
I can’t second this enough. I’d say >95% of my use of default initialized structs is for named/default arguments to functions. If I find that a struct can have sensible independent defaults for some fields during initialization, I’d still rather create a .init method with a Config or Options argument than use default struct values for the fields themselves:
.init is a reasonable default name too for the decl literal, IMO.
Ah, well, often the struct goes deeper, including, say, an ArrayList that wants initial allocation, etc. There may be sub-structs that have their own create(), which needs calling. None of that may need parameters that would be provided in a create() function, but the init() would take a, b, and c “values” that might initialize the simpler stack fields of the struct. Right? init() would then be an error-free track, but create() would do (sub-)allocations. Am I missing something?
Yes, like ArrayList; I use that a bit. I was going to bring it up, but left it off….
Yes, but this is what’s motivating inquiry - I’m not, in fact, writing a lot, as I try to read a lot and learn… but reading, I find variety. I think I’m finally learning more about WHOSE code to favor reading (I think there’s plenty to filter out), and that will help me narrow. But I thought inquiring some about the variety I see made some sense.
There is no rule about init being error free. Any method other than deinit can return errors. However, create normally would return an error, obviously, since it allocates.
That’s an indication there are not very many rules.