zig

How do I correctly use Optional with pointers in Zig to avoid const errors?


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.


Solution

  • 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: