My optional is invalid, but somehow not null?

New to Zig. Playing around with some linked lists for fun. I have this code.

const std = @import("std");

const Node = struct {
    value: i32,
    next: ?*Node,
};

test Node {
    const allocator = std.testing.allocator;

    const n1: *Node = try allocator.create(Node);
    defer allocator.destroy(n1);
    n1.value = 10;

    const n2: *Node = try allocator.create(Node);
    defer allocator.destroy(n2);
    n2.value = 20;

    // Link n1 and n2.
    n1.next = n2;

    var cur: ?*Node = n1;

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }

    std.debug.print("cur.isNull={any}\n", .{cur == null});

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }
}

I’m trying to iterate through the list, going through Node.next. Eventually, cur should be null and I shouldn’t try to do cur.next.

However, the weird thing is that after the 2nd if statement, I would have expected cur to be null, but… it’s not? The 3rd if statement condition is true, but when I try to access the pointer, it crashes because cur was null after all.

$ zig test wat.zig 
10
20
cur.isNull=false
General protection exception (no address available)
/home/user/zigidk/src/wat.zig:37:43: 0x1040df5 in decltest.Node (test)
        std.debug.print("{d}\n", .{(cur.?).value});

Is there another way I’m supposed to check if my optional is valid?

n2.next is undefined, and comparing to undefined is undefined behavior.

allocator.create gives you a pointer to undefined memory.

Here is a version of your code that passes (n2.next is assigned null before it is used).

const std = @import("std");

const Node = struct {
    value: i32,
    next: ?*Node,
};

test Node {
    const allocator = std.testing.allocator;

    const n1: *Node = try allocator.create(Node);
    defer allocator.destroy(n1);
    n1.value = 10;

    const n2: *Node = try allocator.create(Node);
    defer allocator.destroy(n2);
    n2.value = 20;

    // Link n1 and n2.
    n1.next = n2;
    n2.next = null; // < ----- need to initialize n2.next

    var cur: ?*Node = n1;

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }

    std.debug.print("cur.isNull={any}\n", .{cur == null});

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }
}

And in debug build mode, undefined memory is filled with 0xaa, which I guess is != null in this case.

https://ziglang.org/documentation/master/#undefined

Is there a way to make Node.next default to null? Or is there a way to avoid accidentally using undefined as a value?

I tried this, but I still get the crash.

const std = @import("std");

const Node = struct {
    value: i32,
    next: ?*Node = null, // <--- seems to be ignored?
};

test Node {
    const allocator = std.testing.allocator;

    const n1: *Node = try allocator.create(Node);
    defer allocator.destroy(n1);
    n1.value = 10;
    //n1.next = null; // <-- if I add this, then it works.

    var cur: ?*Node = n1;

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }

    std.debug.print("cur.isNull={any}\n", .{cur == null});

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }
}

Also,

in debug build mode, undefined memory is filled with 0xaa

is there a way to print this out?

A default value may not be the most useful thing in this case. In your case, a null in a linked list is just as useful or important as a non-null.

Something a bit more idiomatic would be to provide an init function. By convention, providing an init function communicates to others that your struct needs special care on initialization. It can also help you not forget to initialize some fields.

std.debug.print("uninitialized memory: {x}\n", .{std.mem.asBytes(n1)});
const std = @import("std");

const Node = struct {
    value: i32,
    next: ?*Node,

    fn init(value: i32, next: ?*Node) Node {
        return Node{ .value = value, .next = next };
    }
};

test Node {
    const allocator = std.testing.allocator;

    const n1: *Node = try allocator.create(Node);
    defer allocator.destroy(n1);
    std.debug.print("uninitialized memory: {x}\n", .{std.mem.asBytes(n1)});

    const n2: *Node = try allocator.create(Node);
    defer allocator.destroy(n2);

    // Link n1 and n2.
    // use struct initialization
    n1.* = Node{ .value = 10, .next = n2 };
    n2.* = Node{ .value = 20, .next = null };
    // or use init function
    n1.* = Node.init(10, n2);
    n2.* = Node.init(20, null);

    var cur: ?*Node = n1;

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }

    std.debug.print("cur.isNull={any}\n", .{cur == null});

    if (cur != null) {
        std.debug.print("{d}\n", .{(cur.?).value});
        cur = (cur.?).next;
    }
}

You will also find that it is very common to use the de-reference operator n1.* when initializing the result of allocator.create because it overwrites the entire memory pointed to by n1.

n1.* = Node{ .value = 10, .next = n2 };

output:

$ zig test test.zig 
uninitialized memory: { aa, aa, aa, aa, aa, aa, aa, aa, aa, aa, aa, aa, aa, aa, aa, aa }
10
20
cur.isNull=true
All 1 tests passed.
1 Like

Ooooooh. OK. I see.

I also found this doc: Allocation is not Initialization (mentions workarounds)

… realize that any type of allocation only produces uninitialized space in memory … allocation is fundamentally not initialization because it concerns itself only with reserving memory, not the value at that memory location.

3 Likes

There is also proposal to allow passing default value to allocator.create here Proposal: pass default value to `std.mem.Allocator.create` etc · Issue #20683 · ziglang/zig · GitHub

1 Like