Confusing Syntax for Declaring Allocators in Zig

Confusing Syntax for Declaring Allocators in Zig

Hi everyone,

I’m trying to understand the syntax for declaring allocators, but it’s a bit confusing to me. For example, I’ve seen code like this:

var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;

Here, std.heap.GeneralPurposeAllocator(.{}) looks like a function call being used in place of a type, but in simpler declarations, like var num: i32 = 22;, the type is just plainly i32 without any call-like syntax.

Why do we use this “function call” style for the allocator type? Is GeneralPurposeAllocator not a direct type, but something generated at compile time? I’d appreciate an explanation of what’s going on under the hood and why it’s designed this way.

Thanks!

2 Likes

Not sure which version of zig you are using, but GeneralPurposeAllocator has been renamed to DebugAllocator.

this is the signature
pub fn DebugAllocator(comptime config: Config) type
it takes a comptime config and returns a type.
This is how generic types are done in zig, functions that create a type from the args.

DebugAllocator(.{}) calls it with the default config.
and .init is:
pub const init: Self = .{};, a constant which is the default value of the type.

in the future, the default field values will be removed, and init will be a non default instantiation.
This is because its possible to create an invalid instance of the type if you set some fields while leaving others the default, so types in std are moving away from that.

15 Likes

You can tell types and functions apart by their names. Functions start with lower case letter while types start with upper case letters.
Language reference says:

  • If x is callable, and x’s return type is type, then x should be TitleCase.
  • If x is otherwise callable, then x should be camelCase.
3 Likes

… as long as the convention is being followed. The language itself is case agnostic.

2 Likes

The question was about the standard library. If the standard library does not follow the convention we should report it as a bug.

.Debug, .ReleaseSafe, .ReleaseFast, .ReleaseSmall still annoy me a bit.

2 Likes

Can you share an example ?

using arraylist as a simpler example

        //original
        items: Slice = &[_]T{},
        capacity: usize = 0,
        // will become
        items: Slice,
        capacity: usize,
        
        // original
        pub const empty: Self = .{};
        // will become:
        pub const empty: Self = .{
            .items = &.{},
            .capacity = 0,
        };

somewhat contrived, as arraylists .empty is already a non default instantiation. But you get the point.

This is also a good example of why declarations are prefered over default values.
items is a slice to the used memory, and capacity is the real size of the memory.
with default values it is far too easy to forget to set one, and create an invalid state that could do anything from crash to overwriting other data you are using.

1 Like

Thanks for sharing !

Is this going to be preferred over an init method which does the same thing?

There is not an init method, init on the DebugAllocator that we were originally talking about is a declaration much like empty

ArrayList has initCapacity which allocates the specified amount of memory, and initBuffer which uses an existing buffer. Neither of which are equivalent to empty.

Understood.
In general, if there’s a struct with some fields, will initializing them with default values in .empty be preferred over doing it in the init method ?


Edit:

Trying to answer my question, assuming there’s a struct call Foo with fields items and capacity, will .empty and init look like the below:

// default instantiation
pub const empty: Self = .{
    .items = &.{},
    .capacity = 0,
};

// non default instantiation
pub fn init() Foo {
    return .{
        .items = &.{1, 2, 3},
        .capacity = 5,
    };
}

?

If your function is just returning a constant, why is it a function? There are valid reasons, but they are few and far between

empty here is not a default instantiation, which refers to using the default values of fields, which is done by not explicitly setting those values, see the example I gave earlier. Specifically, the parts under the //original comments

If your function is just returning a constant, why is it a function? There are valid reasons, but they are few and far between

I’m referring to a regular struct which has init, deinit, and other methods (not the DebugAllocator and ArrayList examples).

Trying to understand how .empty impacts init() - will such a struct have both going forward, and if so what would they look ?

Its clear why things under //original are not preferred.

My response to that is the same.

If it’s just returning a constant, if you’re not doing anything you need a function for, it shouldn’t be a function.

But I should have stated explicitly, instead of implying. If you need to do some logic, then of course it needs to be a function.

Any further distinction between what empty is and what init() does, is nuance beneath what I have stated above.

1 Like

Thanks, this clears things.

There’s no call-like syntax for i32 because that type isn’t parameterized. But GeneralPurposeAllocator is a function that is called at compile time and returns a struct type – this is how Zig does generics.

In other languages with generics, the generic parameters use special syntax, e.g. Foo<generic parameters> in C++, Foo[generic parameters] in Scala, Foo!(generic parameters) in D … whereas in Zig regular call syntax is used, and comptime and non-comptime parameters can be intermixed. Zig is also different in that comptime functions can return types, whereas in other languages comptime functions are considered to be “templates” that generate different code depending on the generic parameters. (The end result is the same–Zig also generates multiple versions of the code–but the syntax and semantics are different.) Zig’s approach is more unified, but it can be a little confusing to keep track of what is comptime and what is not.

1 Like

As the doc says: “These are general rules of thumb; if it makes sense to do something different, do what makes sense.”

I think that’s a bit too loose. Better would be “For consistency and readability, these guidelines should be followed unless there is good reason not to–if it makes sense to do something different, do what makes sense. For example, if there is an established convention such as ENOENT , follow the established convention.”