Building Blocks Of Zig: Understanding Structs
How to use these fundamental building blocks in Zig
To learn more about Zig and why I think it's an amazing language check out my blog post Zig is The Next Big Programming Language
What is a Struct in Zig?
Like many other programming languages Zig has structs. A struct is a pretty simple and straightforward concept, it's a user defined data model that can contain multiple fields or members. That's a lot of words to say that a struct is a way to group related data together in a logical and structured way.
Traditional OOP languages like C++, C# and Java use classes to achieve the same goal, we all know the old and tired analogies of classes representing a generic concept like animal and then subclasses like dog and cat inheriting from the animal class and adding their own specific behavior and data or implementing interfaces to achieve polymorphism.
Zig like C does not have classes but it does have structs, enums and unions. In Zig structs are the most common way to group related data together.
How to define a Struct in Zig
In Zig you define a struct by using the struct
keyword followed by the name of the struct and then the body of the struct enclosed in curly braces {}
. Inside the body of the struct you define the fields or members of the struct.
Here is an example of a struct that represents a game character:
const Character = struct {
name: []const u8,
health: u32,
stamina: u32,
say_hello: fn ([]const u8) void,
};
Now that we have defined the Character
struct we can create instances of it like this:
const player = Character{
.name = "Ziggy Stardust",
.health = 100,
.stamina = 100,
.say_hello = fn(name: []const u8) void {
std.log.info("Hello, {s}!\n", .{name}),
},
};
// Call the say_hello function
player.say_hello(player.name);
In this example we create a new instance of the Character
struct called player
and we initialize the fields of the struct with the values "Ziggy Stardust"
, 100
, 100
and a function that logs a message to the console.
Super simple right? Fairly similar to how you would define a struct in C. But wait there's more
Struct Fields can have default values
Default values in Zig Structs are executed at comptime(compile time) and are allow the field to be omitted when creating an instance of the struct. This allows us to essentially have optional typesafe fields in our structs that have a default value if not provided.
For example we can modify the Character
struct to have default values for the health
and stamina
fields:
const Character = struct {
name: []const u8,
health: u32 = 100,
stamina: u32 = 100,
say_hello: fn ([]const u8) void,
};
This way when we create a new instance of the Character
struct we can omit the health
and stamina
fields and they will be initialized with the default values, which in this case will be 100
for both fields.
const player = Character{
.name = "Ziggy Stardust",
.say_hello = fn(name: []const u8) void {
std.log.info("Hello, {s}!\n", .{name}),
},
};
Structs can be packed
By default structs in zig do not maintain a specific order of fields regardless of the order in which you define the fields in the struct. This is not always optimal since sometimes you may wanna have a specific order of your fields to optimize memory usage or interact with certain libraries like OpenGL that require a specific order of fields in a struct. for this we can use the packed
keyword when defining a struct to ensure that the fields are ordered in the same order as they are defined in the struct.
Additionally there will be no padding between the fields in a packed struct.
packed structs can participate in Bit Cast or Pointer Cast operations including during comptime.
const Character = packed struct {
name: []const u8,
health: u32,
stamina: u32,
say_hello: fn ([]const u8) void,
};
Now the bytes of the struct will be ordered in the same order as they are defined in the struct. Simple as that.
Struct fields can be undefined
If you are not yet ready to set a value to a field in a zig struct, you can use the undefined
keyword to set the field to an undefined state. This is useful when you want to create a struct with some fields that you will set later on in your code.
const Goblin = Character{
.name = undefined,
.health = 100,
.stamina = 100,
.say_hello = fn(name: []const u8) void {
std.log.info("Hello, {s}!\n", .{name}),
},
}
In this example we create a new instance of the Character
struct called Goblin
and we set the name
field to undefined
and the health
and stamina
fields to 100
. This way we can create a new instance of the Goblin
struct and set the name
field later on in our code, perhaps we wanna give the goblin a randomly generated name when the goblin enters the view of the player.
Struct can have Methods / Functions
You may have noticed that we have a function as a field in our Character
struct. Zig like many other languages allows you to define functions as struct fields which can be called on instances of the struct.
The say_hello
field in the Character
struct is a function that takes a []const u8
parameter and returns void
meaning no value is returned. But we could for example have a function that allows us to attack another character by passing in the character reference as a parameter.
const Character = struct {
name: []const u8,
health: u32,
stamina: u32,
say_hello: fn ([]const u8) void,
attack: fn (target: *Character) void,
};
const player = Character{
.name = "Ziggy Stardust",
.health = 100,
.stamina = 100,
.say_hello = fn(name: []const u8) void {
std.log.info("Hello, {s}!\n", .{name}),
},
.attack = fn(target: *Character) void {
std.log.info("{s} attacks {s}!\n", .{player.name, target.name}),
},
};
const enemy = Character{
.name = "Goblin",
.health = 50,
.stamina = 50,
.say_hello = fn(name: []const u8) void {
std.log.info("Hello, {s}!\n", .{name}),
},
.attack = fn(target: *Character) void {
std.log.info("{s} attacks {s}!\n", .{enemy.name, target.name}),
},
};
player.attack(&enemy);
enemy.attack(&player);
In this example we have added an attack
function to the Character
struct that takes a pointer to another Character
as a parameter and logs a message to the console. We then create two instances of the Character
struct called player
and enemy
and call the attack
function on each of them passing in the other character as a parameter.
Optimally we'd also want to add a take_damage
function to the Character
struct that would reduce the health of the character when attacked. But this blog post is purely educational.
Structs can be returned from a function which results in a Generic
In Zig like many other languages you can return a struct from a function. But where it gets super interesting is how you can leverage that to create generics Here
fn GoblinHorde(comptime T: type) type {
return struct {
pub const Goblin = struct {
prev: ?*Goblin,
next: ?*Goblin,
data: T,
}
first: ?*Goblin,
last: ?*Goblin,
len: usize,
}
}
Conclusion
Struct are a fundamental building block of Zig and are used to group related data together in a logical and structured way. Structs can have default values, be packed, have undefined fields, have methods and be returned from functions which results in a generic. Structs are a powerful and flexible way to model data in Zig and are used extensively in the standard library and in many third party libraries.