Search code examples
structtypeszig

How are variables in a Zig struct meant to be declared?


While working on a program, I accidentally declared the variables in a struct incorrectly. The strange thing is that it actually worked and I'm not entirely sure why. I'm submitting this question to hopefully get some answers as to why this compiles and what the program is actually doing.

Here's the code of a short example program I made:

const std = @import("std");

//Standard Struct
const Regular = struct {
    message: [:0]const u8 = undefined,
    counter: u8 = 0,

    pub fn init(self: *Regular) void {
        self.message = "Hello from Regular";
        self.incrementCounter();
    }

    fn incrementCounter(self: *Regular) void {
        self.counter += 1;
    }

    pub fn print(self: *Regular) void {
        std.debug.print("Counter: {}, Message: {s}\n", .{ self.counter, self.message });
    }
};

//Weird Struct
const Weird = struct {
    var message: [:0]const u8 = undefined;
    var counter: u8 = 0;

    pub fn init() void {
        message = "Hello from Weird";
        incrementCounter();
    }

    fn incrementCounter() void {
        counter += 1;
    }

    pub fn print() void {
        std.debug.print("Counter: {}, Message: {s}\n", .{ counter, message });
    }
};

pub fn main() !void {
    var a = Regular{};
    const b = Weird;

    a.init();
    b.init();
    a.print();
    b.print();

    var counter: u8 = 0;

    while (counter < 20) : (counter += 1) {
        a.incrementCounter();
        b.incrementCounter();
    }

    a.print();
    b.print();
}

Output:

Counter: 1, Message: Hello from Regular
Counter: 1, Message: Hello from Weird
Counter: 21, Message: Hello from Regular
Counter: 21, Message: Hello from Weird

Regular matches the standard struct architecture I've found in most examples from the documentation as well as Ziglings. Weird is this thing I accidentally made. Somehow this is still valid Zig and runs as I would expect, but there are a few quirks I don't quite understand.

For example, b cannot be a variable. If I attempt it, I get the error: "Error: variable of type 'type' must be const or comptime". Which is odd, since I'm expecting the type of b to be filename.Weird. Otherwise, it works how I would expect it. In the original program I discovered this in, I only discovered this by having to use a pointer to a Weird-type struct and finding numerous errors until I refactored it to a Regular-type struct. So what exactly is happening in Weird? Why does it work? How does it work?


Solution

  • Inside Weird message and counter are container level variables. This works for you here because the struct methods in Weird are using the container level variables (but it may not always work as you expect as described below). You can access these from the outside through the Weird type, e.g., Weird.message and Weird.counter.

    You are getting an error with var b = Weird; because Weird is a type, and types aren't available at runtime, so you have to use const or comptime to assign the type to an identifier.

    This differs from var a = Regular{}; which creates an instance of the Regular struct.

    Struct Fields Are Not Container Level Variables

    In the posted code a is a Regular struct, while b is the struct type Weird. The ramifications of this are that a.incrementCounter() is incrementing the counter field of the struct a which is of type Regular, and there could be many independent instances of Regular structs. But b.incrementCounter() is incrementing the container level variable Weird.counter. This counter is static, and there is only one instance of it.

    If you add these lines to the end of the posted program:

    const c = Weird;
    c.init();
    c.message = "This is a container level message!";
    c.print();
    b.print();
    

    and then compile and run the program, this is the output:

    $ zig run container_variables.zig 
    Counter: 1, Message: Hello from Regular
    Counter: 1, Message: Hello from Weird
    Counter: 21, Message: Hello from Regular
    Counter: 21, Message: Hello from Weird
    Counter: 22, Message: This is a container level message!
    Counter: 22, Message: This is a container level message!
    

    Here c is bound to the type Weird just as b is. Calling c.init() increments the container level variable Weird.counter and c.message = ... assigns a new string message to Weird.counter. Subsequent calls to b.print, c.print, or Weird.print will all show the same results since this method is printing the values of container level variables.