fn createWidgets(allocator: std.mem.Allocator) !void {
// allocate some long-lived memory and store a pointer to it somewhere
}
fn processSomething(allocator: std.mem.Allocator) !void {
// use the allocator to create some scratch memory, use it, then free it
}
These are two very different usages of the allocators, so what’s the Ziggy way to hint to the user of these functions what the memory usage pattern is? That processSomething() would be happy with an arena allocator, but createWidgets() probably wouldn’t? Is it a code-smell to make this kind of thing visible outside the function? Is this just a matter of writing a helpful comment on the function?
The Ziguanic way of doing this is to pass the scratch memory slice into the processSomething function instead of allocating in a processing function.
First, can you explain what you mean when you say “probably wouldn’t”? I think they’d technically be fine in either case. For example, I worked a lot with trees recently, and it was very nice to have reusable arenas to allocate the nodes onto. You can build contiguously, and then free them all at once and use the same memory for a new tree.
In terms of signaling? It’s really important to begin by understanding what the std.mem.Allocator signals itself.
The allocator interface (std.mem.Allocator) is a struct with an *anyopaque pointer and a virtual table. It erases the type passed to it and provdies an interface that connects the the virtual table under the hood:
So what is this signaling to being with? I’d argue the allocator interface basically is like saying “hey, use anything you’d like. This is an interface, after all, and if you satisfy the interface requirements then you’ve got the job.” Remember, once you become an interface, we forget who and what you were - it’s like that old song “giving you a number and taking away your name” (which is a strangely effective analogy for what an opaque pointer does) lol.
So in other words, by the time you get to this position, using the allocator interface already means that you’ve surrendered control to the user.
Not sure I’d agree with that. I’d say there’s no real convention for the particular distinction in the OP.
For a random example that blurs the lines and therefore makes your suggested strategy effectively impossible, see std.fs.path.relative. It takes an Allocator and it uses it for both allocating scratch intermediates that will be freed before return and for the slice that is eventually returned.
Maybe in a case like relative it might make sense to take two allocator parameters, but there’s not much precedence for that as of yet.
The only real convention I’ve seen for signalling what type of allocator an Allocator parameter is expecting is naming the parameter gpa (short for general purpose allocator) if it doesn’t really care about what type of allocator it is, and arena if it expects/requires an arena allocator.
I don’t usually disagree with ya, but in general I do hold that it’s preferable to allocate memory before hand where possible. I understand that some situations blur the lines (of course, that’s always the case with complex problems)… but in general, I think knowing how much memory a function requires to process something is preferable to relying on allocation to do the heavy lifting.
If the question was about whether we should be allowed to pass allocators to processing functions, I’d completely agree with you here. In terms of recommending what people should prefer, attempting to figure out how much memory is required is preferable to starting with an allocator interface imo.
Same, I think we just interpreted the question in different ways. If it’s possible to provide an API that avoids internal allocation, I agree that’s usually worth pursuing (or providing both an allocating and non-allocating API).
I actually can’t think of too many examples in the standard library that go either direction, though–that is, I can’t think of any functions that take a slice to use purely as a scratch buffer, and I can’t think of any functions that take an allocator purely to use for scratch allocation. I’m sure there are examples, though; would be interesting to know how many examples there are for each.