# Exploring the idea of a more modular alternative design to \`std.mem.Allocator\`

**URL:** <https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589>\
**Category:** Brainstorming\
**Tags:** memory-management, standard-library\
**Created:** [September 13, 2026, 1:24pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589 "2026-09-13T13:24:02Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![kirisoup](https://ziggit.dev/letter_avatar_proxy/v4/letter/k/ac91a4/32.png) [@kirisoup](https://ziggit.dev/u/kirisoup)\
**Post date:** [September 13, 2026, 1:24pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/1 "2026-09-13T13:24:02Z")

</div>

## The thing that inspired this post:

_I remember reading a blog post from [matklad](https://matklad.github.io/) (IIRC that is, I can’t find it unfortunately), which introduced me to the concept of using `arena` v.s. `gpa` to manage the lifetime of internal allocations of functions._

_Edit: I found it, but it’s not a blog post, and the pattern is slightly different than what I remembered: [Allocators: Best Practices & Anti-Patterns - #5 by matklad](https://ziggit.dev/t/allocators-best-practices-anti-patterns/14043/5)_

Consider the following function:

```zig
pub fn doWork(...) struct {
    foo: []const u8,
    bar: *Thing, // might even contain references to other allocated data :O
} {
    ...
}

```

how would you manage the lifetime of `.foo` and `.bar`? In rust, `doWork` might receive a `'a` lifetime and declare `.foo` and `.bar` to be under that lifetime, that is an okay solution, but there is an alternative in zig which I really like, by simply changing the signature of `doWork` to:

```zig
pub fn doWork(arena: Allocator, gpa: Allocator, ...) ...

```

where the returned `.foo` and `.bar` would be allocated under the `arena`, separated from other internal allocations under `gpa`.

What I really like about this approach is that, conventionally a `gpa` is an allocator capable of individual `free`s whereas an `arena` is not. By _naming_ _(we’ll get to that in a sec)_ the arguments this way, the function expresses that it will use the `gpa` to allocate data that are used internally, guaranteeing them to all be freed, whereas the `arena` will be used to allocate data that it is not responsible of freeing. This way the caller can confortably do something like:

```zig
// var arena_instance: std.heap.ArenaAllocator
// const arena = arena_instance.allocator()
{
    const foo, const bar = doWork(arena, gpa, ...);
    defer arena_instance.reset(...);

    // use foo and bar to do intersting stuff
    ...
}
// foo and bar and other inter-referenced memory are now freed
// carry on with other works
...

```

* * *

## The issue:

Now despite how I love this pattern, an obvious point of awkwardness is that the differences between the `arena` and the `gpa` is only conveyed via their names. Imagine if the following is what you are presented with instead, it’d be not obvious at all how each allocators are expected to be used:

```zig
pub fn doWork(allocator_a: Allocator, allocator_b: Allocator, ...) ...
// or
pub const doWork: fn(Allocator, Allocator, ...) ... = ...;

```

_obviously one should avoid irresponsible naming scheme like this, but on the other hand, I often find the zig’s standard library consisting of design choices that naturally encourage good programming patterns, maybe we can do better here as well._

Essentially what we want is a way to represent the capability of `free` isolated from `allocate`, via the type system.

* * *

## Idea:

_Before I stumbled uppon zig I was learning rust, and one idea I still really missed from the language is `trait`. Now I don’t intend to dive into the good and bad of how they implemented the system and whether should or shouldnot we add something simialr to zig, but I do like the abstract idea of separating each atomic properties of a type into a separate trait (e.g. `Read + Write + Seek` instead of a big`File` class)._

Adapting that idea to `std.mem.Allocator`, what if instead of a single:

```zig
// std.mem
pub const Allocator = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    const VTable = struct {
        alloc: *const fn (...) ...,
        free: *const fn (...) ...,
        ...
    };
};

```

we split it up into:

```zig
// std.mem
pub const Allocate = struct {
    vfunc: *const fn (...) ...,
    pub fn create ...
    pub fn alloc ...
    ...
};
pub const Free = struct {
    vfunc: *const fn (...) ...,
    pub fn destroy ...
    pub fn free ...
};
// other capatibilities like remap and resize
...

```

Therefore, the `doWork` example function signature can be changed to something like:

```zig
pub fn doWork(
    // btw: I assume we should use intrusive interface
    // in this case, but I could be wrong.
    arena: *mem.Allocate,  
    gpa: struct { *mem.Allocate, *mem.Free },
    ...,
) ...

```

Just by looking at the signature, you can tell that `doWork` cannot free using the `arena`, and `doWork` will use the `gpa` to free!

Using it from the caller might look something like:

```zig
_ = doWork(
    &arena.interface.allocate,
    .{ &gpa.interface.allocate, &gpa.interface.free },
    ...,
);

```

or maybe if you are into something fancier

```zig
_ = doWork(
    arena.interface(.allocate),
    gpa.interface(.{ .allocate, .free }),
    ...,
);

```

* * *

We can extend the idea further, for example:

```zig
// std.array_list.Aligned

pub fn ensureTotalCapacity(
    self: *Self, 
    gpa: struct {
        in_place: ?*mem.resize.Expand, // optional
        relocating: *mem.Allocate,
    },
    new_capacity: usize
) ...

```

Notice the `mem.resize.Expand`. We could do `mem.Resize`, but I feel like there might be allocator implementations where an in-place shrink can be easier than an in-place expand, or vice versa.

* * *

### Some potential issues/caveates:

- With `gpa: struct { *mem.Allocate, *mem.Free }` it is totally valid to pass the two interfaces from two different allocators. Tho is this really a problem that concerns us? Since if a free frees a memory region not allocated by the allocator in debug/ReleaseSafe build, I would assume it’ll typically lead to a panic.
- An allocator instance might have to effectively store multiple `*const fn`, as opposed to constructing a `struct { ptr: *anyopaque, vtable: *const VTable }`.
- function signature and caller syntax can become more verbose

---

<div class="post-metadata">

**Author:** ![alanza](https://ziggit.dev/user_avatar/ziggit.dev/alanza/32/6006_2.png) [@alanza](https://ziggit.dev/u/alanza)\
**Post date:** [September 13, 2026, 1:29pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/2 "2026-09-13T13:29:32Z")

</div>

Very pretty. I think I missed what concrete problem this proposal is solving, though?

---

<div class="post-metadata">

**Author:** ![kirisoup](https://ziggit.dev/letter_avatar_proxy/v4/letter/k/ac91a4/32.png) [@kirisoup](https://ziggit.dev/u/kirisoup)\
**Post date:** [September 13, 2026, 1:36pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/3 "2026-09-13T13:36:30Z")

</div>

I might have explained it rather poorly.

The thing is that the allocation pattern of gpa allocators and arena allocators can be very different. When interacting with third party libraries and even some of the std library functions, I often have to guess or investigate into internal implemenations to see if it is a good idea to give it an ArenaAllocator (or FixedBufferAllocator), without it using up too many memory due to failed frees (an examaple would be zon deserialization functions in std lib).

It’s fair to say that these (and the examples in the post) are just minor issues, but I still think it can be interesting to have a brainstorm about this idea I had 🙂

---

<div class="post-metadata">

**Author:** ![kirisoup](https://ziggit.dev/letter_avatar_proxy/v4/letter/k/ac91a4/32.png) [@kirisoup](https://ziggit.dev/u/kirisoup)\
**Post date:** [September 13, 2026, 1:53pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/4 "2026-09-13T13:53:04Z")

</div>

Similarly it might be interesting to, for example, seperate `std.Io.async` and `std.Io.concurrent` into their own interfaces, and therefore a function signature would be enough to convey that whether it requires concurrency.

I haven’t worked with the new `std.Io` interface since its addition so am not familiar enough with it to provide more in depth thoughts, but I plan to return to zig programming, probably after 0.17 release 🙂

---

<div class="post-metadata">

**Author:** ![jmstevers](https://ziggit.dev/user_avatar/ziggit.dev/jmstevers/32/9230_2.png) [@jmstevers](https://ziggit.dev/u/jmstevers)\
**Post date:** [September 13, 2026, 2:45pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/5 "2026-09-13T14:45:33Z")

</div>

> [@kirisoup](#):
>
> a function signature would be enough to convey that whether it requires concurrency.

This is already the case.

```zig
fn doWork(io: Io) Io.ConcurrentError!void;

```

`Io.ConcurrentError` has `error.ConcurrencyUnavailable` which lets the caller know that the function will fail if concurrency is unavailable. That’s more flexible than say

```zig
fn doWork(io: IoConcurrent) void;

```

because a function could have a synchronous or asynchronous fallback if concurrency is unavailable. e.g.

```zig
fn doWork(io: Io) void {
    _ = io.concurrent(doOtherWork, .{}) catch {
        // just do work here
    };
}

```

---

<div class="post-metadata">

**Author:** ![kirisoup](https://ziggit.dev/letter_avatar_proxy/v4/letter/k/ac91a4/32.png) [@kirisoup](https://ziggit.dev/u/kirisoup)\
**Post date:** [September 13, 2026, 2:51pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/6 "2026-09-13T14:51:40Z")

</div>

Oh, this is elegant!

---

<div class="post-metadata">

**Author:** ![jmstevers](https://ziggit.dev/user_avatar/ziggit.dev/jmstevers/32/9230_2.png) [@jmstevers](https://ziggit.dev/u/jmstevers)\
**Post date:** [September 13, 2026, 2:55pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/7 "2026-09-13T14:55:49Z")

</div>

> [@kirisoup](#):
>
> a `gpa` is an allocator capable of individual `free`s whereas an `arena` is not.

`std.heap.ArenaAllocator` is able to free things, although only in a LIFO manner.

> Calls to free an individual item only free the item if it was the most recent allocation, otherwise calls to free do nothing.

[std.heap.ArenaAllocator docs](https://ziglang.org/documentation/master/std/#std.heap.ArenaAllocator)  
[std.heap.ArenaAllocator.free](https://ziglang.org/documentation/master/std/#std.heap.ArenaAllocator.free)

---

<div class="post-metadata">

**Author:** ![kirisoup](https://ziggit.dev/letter_avatar_proxy/v4/letter/k/ac91a4/32.png) [@kirisoup](https://ziggit.dev/u/kirisoup)\
**Post date:** [September 13, 2026, 2:58pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/8 "2026-09-13T14:58:38Z")

</div>

For optional concurrency I was more thinking about something like:

```zig
// both should be from the same io instance
fn doWork(io: struct { Io, ?Io.Concurrent }) void

```

But the idea of using `Io.ConcurrentError` to convey the requirement of concurrency is more neat than what I was imagining.

---

<div class="post-metadata">

**Author:** ![kirisoup](https://ziggit.dev/letter_avatar_proxy/v4/letter/k/ac91a4/32.png) [@kirisoup](https://ziggit.dev/u/kirisoup)\
**Post date:** [September 13, 2026, 3:00pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/9 "2026-09-13T15:00:32Z")

</div>

I am aware of that, the term “arena” I was using in the post is referring to a more abstract “arena allocation pattern” instead of our ArenaAllocator implementation.

Tho thanks for pointing it out still, I was going to clarify this but I guess I forgot 😄

---

<div class="post-metadata">

**Author:** ![gwenzek](https://ziggit.dev/user_avatar/ziggit.dev/gwenzek/32/8137_2.png) [@gwenzek](https://ziggit.dev/u/gwenzek)\
**Post date:** [September 13, 2026, 3:06pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/10 "2026-09-13T15:06:19Z")

</div>

I do have a lot of functions that need a allocator for temp allocations as well as returning the result.

Currently I generally pass a single allocator that I use directly to allocate the result, and also to create a temp arena inside my function.

This is somewhat suboptimal though because I may end up with several arena on the call stack, and prevent reuse of the arena.

I could pass two allocators, but as you said it feels error prone. I’m also considering the pattern of passing a `*Arena` to function that could use one, but I don’t think it solves everything. Should the arena be assumed empty ? Is the caller or the callee reponsible to clean it? If it’s the callee how does it avoid freeing caller memory?

---

<div class="post-metadata">

**Author:** ![kirisoup](https://ziggit.dev/letter_avatar_proxy/v4/letter/k/ac91a4/32.png) [@kirisoup](https://ziggit.dev/u/kirisoup)\
**Post date:** [September 13, 2026, 3:18pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/11 "2026-09-13T15:18:05Z")

</div>

In the `doWork` example in my post the `arena` should only be for ~~passing data out to the caller~~ _allocating data that are referenced by the function result_, the function should ideally not allocate internal temporary data there, nor should the function reset the arena (therefore why later down the post the `arena: Allocator` can be replaced with `arena: *mem.Allocate`).

I have also written functions (I say functions but I think there’s really just one) which benefitted from a temp internal arena, but I allocate the arena within the `gpa` (the equivalent `gpa` in the `doWork` example), since the `gpa` is for any allocations (even allocations of allocators) that doesn’t outlive the function lifetime.

---

<div class="post-metadata">

**Author:** ![gwenzek](https://ziggit.dev/user_avatar/ziggit.dev/gwenzek/32/8137_2.png) [@gwenzek](https://ziggit.dev/u/gwenzek)\
**Post date:** [September 13, 2026, 5:47pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/12 "2026-09-13T17:47:26Z")

</div>

Ok I misread, but then the roles are just reversed wrt to what I said. In any case you have a temporary allocator and an out allocator. I’m not sure why it’s so important to you that the out allocator must only be an arena then.

---

<div class="post-metadata">

**Author:** ![WeeBull](https://ziggit.dev/user_avatar/ziggit.dev/weebull/32/6828_2.png) [@WeeBull](https://ziggit.dev/u/WeeBull)\
**Post date:** [September 13, 2026, 7:46pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/13 "2026-09-13T19:46:13Z")

</div>

I think you’re conceptually missing something in your descriptions of different types of allocator.

The purpose of the `std.mem.Allocator` generic type is to allow functions to be written without caring about about which type of allocator they’re using. The same code runs fine under a GPA, or an arena, or a fixed-buffer allocator. It’s down to the caller to determine the strategy for memory allocation. That’s part of the idea of writing code that can be reused in multiple scenarios.

As soon as you rely on an allocator being an arena, and therefore write code that never releases anything, you’ve placed a constraint on where your code can be used. I think your type tree takes you even further down this road by stating arena allocators must be explicit.

---

<div class="post-metadata">

**Author:** ![QuaternionWake](https://ziggit.dev/user_avatar/ziggit.dev/quaternionwake/32/6355_2.png) [@QuaternionWake](https://ziggit.dev/u/QuaternionWake)\
**Post date:** [September 13, 2026, 8:56pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/14 "2026-09-13T20:56:08Z")

</div>

Except that it can be pretty difficult to make code play well with all kinds of allocators.

Sure, sometimes it’s easy, when the allocation pattern is simple (e.g. the most trivial case: a function that makes one allocation).  
But if your code has to deal with a messed up web of object lifetimes, then it’s gonna be very difficult to have no leaks with a GPA, or on the other hand if it uses a container like an arraylist then it’s gonna use way more memory with an arena then with an allocator capable of freeing.

I wanted to bring up some examples of even the std lib having `arena: Allocator` function arguments, but looking at it now it seems the one I first remember running into is no longer there. Another one still is however, in `std.process.Args`: `pub fn toSlice(a: Args, arena: Allocator) ToSliceError![]const [:0]const u8`

Looking through the standard library though, it seems that most uses of that pattern are farther away from the user facing parts of the stdlib, so I’m assuming there might be an effort to move away from it?

---

<div class="post-metadata">

**Author:** ![kirisoup](https://ziggit.dev/letter_avatar_proxy/v4/letter/k/ac91a4/32.png) [@kirisoup](https://ziggit.dev/u/kirisoup)\
**Post date:** [September 13, 2026, 9:13pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/15 "2026-09-13T21:13:13Z")

</div>

I don’t think you get the idea. It is not dependency over the allocator being an ArenaAllocator, or FBA, or any of the other concrete allocator implementations.

It is simply splitting std.mem.Allocator into multiple modular interfaces, Allocate, Free and Resize, thus functions can clearly express that for a certain allocator argument, for example, it only need the allocate side of the allocator, not the free.

---

<div class="post-metadata">

**Author:** ![npc1054657282](https://ziggit.dev/user_avatar/ziggit.dev/npc1054657282/32/5790_2.png) [@npc1054657282](https://ziggit.dev/u/npc1054657282)\
**Post date:** [September 13, 2026, 9:27pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/16 "2026-09-13T21:27:54Z")

</div>

> [@WeeBull](#):
>
> The same code runs fine under a GPA, or an arena, or a fixed-buffer allocator.

Just because something runs fine doesn’t mean the code is correct. E.g., in dynamic data structures like ArrayList, the allocator parameter is named `gpa` to hint to users not to use the bump arena allocator, as that can easily waste half the memory. But this kind of naming-only constraint is easy to overlook, leading developers to write code that seems to work but is actually quite suboptimal.

---

<div class="post-metadata">

**Author:** ![kirisoup](https://ziggit.dev/letter_avatar_proxy/v4/letter/k/ac91a4/32.png) [@kirisoup](https://ziggit.dev/u/kirisoup)\
**Post date:** [September 13, 2026, 9:59pm UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/17 "2026-09-13T21:59:20Z")

</div>

Here’s a random example of miss-use of an ArenaAllocator in the place of a gpa:

[kostya/benchmarks/brainfuck/bf.zig#L190 (github)](https://github.com/kostya/benchmarks/blob/5bc68b3705fddd0fdb16b8748c4f2c73e1032a43/brainfuck/bf.zig#L190)

I guess for this kinds of “language comparison” projects you usually don’t expect the best code quality 😄, but for example if let’s say ArenaAllocator is changed to only expose an Allocate interface and no Free, funny mistakes like this would be less likely to happen.

---

<div class="post-metadata">

**Author:** ![ScottRedig](https://ziggit.dev/user_avatar/ziggit.dev/scottredig/32/5880_2.png) [@ScottRedig](https://ziggit.dev/u/ScottRedig)\
**Post date:** [September 14, 2026, 12:03am UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/18 "2026-09-14T00:03:11Z")

</div>

Something to consider is that std design has very different constraints from those in your own projects. In the std, being general when reasonable (so, taking Allocator in this case) is good because it works for more use cases. You however know your use cases, and so can easily impose constraints on use cases to simplify your code. For memory allocator, you can trivially pass around pointers to ArenaAllocator instead of the generic Allocator. If you don’t need or want a different allocator for that code, just constrain it as such.

---

<div class="post-metadata">

**Author:** ![shimekukuri](https://ziggit.dev/letter_avatar_proxy/v4/letter/s/cab0a1/32.png) [@shimekukuri](https://ziggit.dev/u/shimekukuri)\
**Post date:** [September 14, 2026, 3:01am UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/19 "2026-09-14T03:01:56Z")

</div>

I think that there is actually something wrong with your pattern. Really you shouldn’t be passing in two separate allocators. An arena is backed by another allocator, and you should be storing the arena inside of your struct. I’m really not sure what situations you are rubbing into where a struct has allocations that live one lifetime, but owned data last lives another. I would think of you were in a situation where you would need a field to persist beyond the lifetime of the struct that you would implement something such as toOwned where the recipient is returned the valid pointer and the originating data, and the struct sets either a null pointer or runs deinit.

I think that you are trying to solve a problem through the lenses that rust provides to you instead of the tools and patterns that full manual memory management provides to you.

---

<div class="post-metadata">

**Author:** ![shimekukuri](https://ziggit.dev/letter_avatar_proxy/v4/letter/s/cab0a1/32.png) [@shimekukuri](https://ziggit.dev/u/shimekukuri)\
**Post date:** [September 14, 2026, 3:13am UTC](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589/20 "2026-09-14T03:13:18Z")

</div>

Is it though? Sure there are particular cases where gpa is better then arena performance wise, and there is a lot of nuance, but your comment to me seems to conflate performance with correctness. Allocators have several characteristics beyond Just performance that may make it the more correct choice, such as arenas modeling a particular lifetime of related data.

[Next page](https://ziggit.dev/t/exploring-the-idea-of-a-more-modular-alternative-design-to-std-mem-allocator/17589.md?page=2)
