Optionally reducing memory allocations

Hi,

I’m might be overthinking this but I was wondering if someone could give me some best practices relating to optional memory allocations.

As a toy project I’m looking to implement a client for hashicorp consul.

At a high level I’m thinking of having something like this so that if you know that your address string will outlive your consul struct, you can skip allocating by passing in a null allocator. Or are there better, already established ways of doing something like that?

const Consul = struct {
    allocator: ?std.mem.Allocator = null,
    address: ?[]const u8 = null,

    const DefaultAddress: []const u8 = "http://localhost:8500";

    const ConsulInitError = error{
        MissingAllocator,
    };

    pub fn init(allocator: ?std.mem.Allocator, address: ?[]const u8) !Consul {
        if (address) |addr| {
            if (allocator) |alloc| {
                return .{ .allocator = alloc, .address = try alloc.dupe(u8, addr) };
            }
            return ConsulInitError.MissingAllocator;
        }
        return .{};
    }

    pub fn deinit(self: *Consul) void {
        if (self.allocator) |alloc| {
            if (self.address) |addr| {
                alloc.free(addr);
                self.address = null;
            }
        }
    }

    pub fn getAddress(self: Consul) []const u8 {
        return self.address orelse DefaultAddress;
    }
};

Thanks.

Hey @slar, welcome to the forums.

The first thing I would recommend is to decouple your allocator from your structure.

From the code you have provided, the only time you actually use your allocator is in the init and deinit functions (if there are other times, then feel free to post those). Essentially, this means you could instead pass in an already allocated address and that will circumvent this entire problem (you may have other issues, granted).

So, take ArrayList for instance - it uses the allocator quite extensively depending on how you use the type to ensure invariant relationships are established and maintained. In the case you’ve presented here, you’re using it as a one time dupe and free. You don’t need the internal control flow here and you’ll end up having to do strange things to make this behave differently.

Null Allocators are particularly good for allocator composition. For instance, say I have a stack based allocator that also has a backing allocator; when the stack runs out of memory, it uses the backing allocator instead. If I want to ensure that only the stack is ever used, setting the backing allocator to the null allocator is a way to signal that. At the very least, everything still is just allocation all the way down.

3 Likes

Thanks, looks like I was indeed overthinking things, coming from C++ I think I feel the need to encapsulate everything.

I probably just simplify it to this so the usage is just creating the struct and leave any allocations to the caller.

const Consul = struct {
    address: []const u8 = "http://localhost:8500"
    
     // other functions like getService etc...
};
1 Like

I strongly understand this sentiment (C++ guy here myself). This is still one of the trickiest points of translation for many people. If this is the path you’re going to take, I recommend marking the solution provided above (sometimes people forget) :slight_smile:

1 Like