What are your thoughts on adding allocPrint to the allocator interface?

Before your reading this: I got my own allocator interface which is a shallow wrapper of the std version. My question is whether I should add allocPrint to my own allocator.

I use allocPrint quite a lot (78 times to be precise), and every time it ends up being this overly long line filled with noise:

const path1 = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/cubyz/blocks/textures/{}.png", .{i}) catch unreachable;
// or
const path1 = try std.fmt.allocPrint(main.stackAllocator.allocator, "assets/cubyz/blocks/textures/{}.png", .{i});
// If I were to add `allocPrint` to my allocator interface it could look like this:
const path1 = main.stackAllocator.print("assets/cubyz/blocks/textures/{}.png", .{i});

I personally think the last one is more readable since it has less noise.
I’m just not sure if this is a good idea conceptually, since printing stuff is not really the Allocators concern.
So that’s why I wonder: What do other people think about this?

For pathes I wonder whether you would need some more complicated mixture of std.fs.path.join (for the path) and allocPrint (for the filename) to make the code work on windows? (But I haven’t dealt with windows in a while)

I am assuming that this isn’t specifically about filenames with placeholders?


1. Helper on the Stack

I think if I was in that situation my first approach would be to play around with and consider a helper data structure on the stack, like this:

const helper:Helper = .{ .allocator = main.stackAllocator };
const path1 = helper.print("assets/cubyz/blocks/textures/{}.png", .{i});
const path2 = helper.print("assets/cubyz/blocks/textures/{}.png", .{i});

I think that has the benefit that the helper can have arbitrary relatively low effort methods that are tailored to making some specific code look less cluttered.

But if the uses of print are single calls scattered throughout different functions, than declaring that helper may not actually help with reducing clutter. So basically this is only helpful if there are many calls within specific functions.

2. Helper built into some common Data Structure

Another approach may be to try and group the calls that are currently scattered through out many different functions and instead collect them in a helper, for example if you have many different kinds of pathes that get generated, you could create a thing in your main structure that handles all the different path kinds, I imagine something along the lines of:

const path1 = main.path(.block_texture, i) catch unreachable;
// or
const path1 = try main.path(.block_texture, i);

And for the path function I would imagine that it would be defined something like this:

const Pathes = struct {
    const Kind = enum { block_texture, icon, background, character_portait, ... };
    // somewhere has a mapping from Kind to path prefix

    // either allocates the pathes every time or maybe caches them in a hashmap
    pub fn get(self:*Pathes, allocator:std.mem.Allocator, comptime kind:Kind, arg:anytype) ![]const u8 {
        // ...
    }
};
const Main = struct {
   // other fields and functions
   pathes: Pathes,

   pub fn path(self:*Main, comptime kind:Pathes.Kind, arg: anytype) ![]const u8 {
      return self.pathes.get(self.stackAllocator, kind, arg);
   }
}

Depending on what main is and contains, you also could avoid the extra path function and just use main.pathes.get directly.


I think only when I didn’t find a way, to somehow put a helper data structure (that basically groups the part that is messy into one implementation that can handle all the cases (either because there is only one kind, or by separating between different kinds)), then I would consider adding such a method to the allocator interface.

I think it can be helpful to have one specific part of the application, that handles filenames. Basically I don’t want the knowledge about what filename prefixes are used scattered through the codebase, but instead have one file that maps all the different kinds of prefixes (either statically or at runtime) to their actual values and then only have the shorter kind values scattered through the code base.

If this grouping works, it also allows you to make the decision for what allocator to use etc. at the same time, either for specific groups or all groups, thus eliminating the need to specify these things multiple times, for every call.

2 Likes

Thanks for the detailed answer.

No, actually Windows does seem to handle forward slashes fine these days.
At least so far I haven’t seen any problems with it.

But this might actually be a better alternative to allocPrint, at least for the paths that only contain strings.

Yeah, it’s scattered throughout 30 different files.
The helper on the stack does seem like a nice pattern though.

That is a good point.

It seems that most of my allocPrints come from paths and chat messages that get sent to the user.
So I guess the best solution would really be to find helper structs or functions for these two use cases instead of changing how allocPrint works.

1 Like