During the study and practice of optionals and pointers in Zig, I encountered behavior that was quite unclear to me. I can't quite understand what exactly the compiler is trying to tell me.
In the code, I have an Optional next where I want to assign a memory reference or say that it is nullable. However, the compiler gives me the following error:
error: expected type '?*SinglyLinkedList.Node', found '*const SinglyLinkedList.Node'
currentNode.*.next = &newNode;
I can't fully understand why const
is used here instead of Optional
(?
).
const Node = struct {
value: u8,
next: ?*Node,
};
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
var head = Node{
.value = 0,
.next = null,
};
addNode(&head, 1);
addNode(&head, 2);
addNode(&head, 3);
// printList(head);
try stdout.print("Hello, {s}!\n", .{"world"});
}
pub fn addNode(head: *Node, value: u8) !void {
var currentNode = head;
while (currentNode.*.next != null) {
currentNode = currentNode.*.next orelse break;
}
const newNode = Node{
.value = value,
.next = null,
};
currentNode.*.next = &newNode orelse unreachable;
}
Based on the error, I thought I shouldn't use
orelse
and should just use &newNode
. But it still shows the error.
Zig version is 0.14.0.
The main problem with your code is that you are not allocating the memory properly.
When you say:
pub fn addNode(head: *Node, value: u8) !void {
...
const newNode = Node{
.value = value,
.next = null,
};
}
you are creating newNode on the stack of the function addNode
, so the pointer to this memory becomes invalid as soon as the function returns.
An easy way to tackle this is to pass an allocator down ton addNode
, which allocates separate memory, which will remain live after addNode()
.
pub fn main() !void {
// a simple allocation strategy that uses main()'s stack
// and a fixed buffer o 1024 bytes.
var buf: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const allocator = fba.allocator();
addNode(allocator, &head, 1);
// ...
}
pub fn addNode(allocator: std.mem.Allocator, head: *Node, value: u8) !void {
// ...
const newNodePtr = try allocator.create(Node);
newNodePtr.* = Node{
.value = value,
.next = null,
};
currentNode.next = newNodePtr;
}
Note that the pointer newNodePtr
is still const
, but that does not say anything about the value behind that pointer; it merely means that we don't change the value of the pointer.
Notice also that buf
is what holds the individual bytes, and this part is variable. fba
is what holds a reference to buf
, plus other fields that keep track of which part of buf
is occupied, etc. so that needs to be variable as well.
allocator
, on the other hand only holds reference to fba
, and that is not going to change, so allocator
is const
. (I could have also passed fba.allocator()
directly, which also makes it const since function arguments are always const in Zig.)
Finally, allocator.create()
is "talking" to the fba
in the background.
Here's altered version of your code:
const std = @import("std");
const Node = struct {
value: u8,
next: ?*Node,
};
fn printList(head: *Node) void {
var currentNode = head;
while (currentNode.next) |next| {
std.log.warn("node.value={d}", .{currentNode.value});
currentNode = next;
}
std.log.warn("node.value={d}", .{currentNode.value});
}
pub fn main() !void {
var buf: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const allocator = fba.allocator();
var head = Node{
.value = 0,
.next = null,
};
try addNode(allocator, &head, 1);
try addNode(allocator, &head, 2);
try addNode(allocator, &head, 3);
printList(&head);
}
fn addNode(allocator: std.mem.Allocator, head: *Node, value: u8) !void {
var currentNode = head;
while (currentNode.next) |next| {
currentNode = next;
}
const newNodePtr = try allocator.create(Node);
newNodePtr.* = Node{
.value = value,
.next = null,
};
currentNode.next = newNodePtr;
}
I've made several extra changes:
addNode
does not need to be pub
since it's only used within this main module.
You don't need to say currentNode.*.next
; Zig will automatically dereference pointers in this context.
I've simplified the while
loop and used capture. This gets rid of the optionality since the loop will only run if currentNode.next
is not null, and next
will be of type *Node
, not ?*Node
.
Notice the newNodePtr.* = Node{..}
syntax; this is equivalent to creating new Node and copying it to the pointer address.
I've renamed newNode
to newNodePtr
. It's not very common to add this suffix, but I found it helpful when learning Zig.