0

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.

4
  • 3
    It's const because const newNode is … const Commented Mar 30 at 18:23
  • so if i will change from const to var it should be fixed ? I tried it, and still have the same error about var and optional Commented Mar 31 at 12:06
  • Well the code you show has several other issues, but yes switching to var compiles for the assignment shown in the error currentNode.*.next = &newNode; Commented Mar 31 at 12:44
  • As mention var doesn't fixed the issue. So i just rewrite this code, to allocate memory (not just pointers). Thanks for suggestions Commented Apr 1 at 12:58

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.