Heap allocated struct with default null value Segfaults

Hello everyone,

I am very new to Zig, please forgive me if I should have known this already.

I have the following zig code:

const std = @import("std");

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

fn printNodes(n: *const Node) void {
    var maybe_node: ?*const Node = n;
    while (maybe_node) |node| {
        std.debug.print("node.value {d}\n", .{node.value});
        std.debug.print("node 0x{x:0>24}\n", .{@as(u96, @bitCast(node.*))});
        maybe_node = node.next;
    }
}

pub fn main() !void {
    const stackNode: Node = .{ .value = 1 };
    printNodes(&stackNode);

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    var alocator = gpa.allocator();

    var heapNode: *Node = try alocator.create(Node);
    defer alocator.destroy(heapNode);
    heapNode.value = 1;
    printNodes(heapNode);
}

looping over stack allocated struct works expected:

node.value 1
node 0x000000000000000000000001

But the code segfaults when looping over heap allocated struct with the following error:

node.value 1
node 0xaaaaaaaaaaaaaaaa00000001
Segmentation fault at address 0x0
zig/learn/struct_default_null.zig:8:51: 0x10ff0b8d3 in printNodes (struct_default_null)
        std.debug.print("node.value {d}\n", .{node.value});
                                                  ^
???:?:?: 0xa075894898558947 in ??? (???)
Unwind information for `???:0xa075894898558947` was not available, trace may be incomplete

[1]    20380 abort      zig run struct_default_null.zig

I only used packed struct because i cannot @bitCast normal struct, but it also crashes.
I can see the value of the next is undefined in heap allocation.
Why is it like that and why the while loop even does next iteration? Shouldn’t the next be null?

You did not initialize next field, only value field, next can be anything after allocation.
See this doc.

4 Likes
    heapNode.value = 1;
    heapNode.next = null;
    printNodes(heapNode);

and it works:

$ ./1 
node.value 1
node 0x000000000000000000000001
node.value 1
node 0x000000000000000000000001

A default value is a comptime-known value that is implicitly passed when using the Type{ .field = value, ... } syntax. But you’re creating a new one with an allocator it’s like using var instance: Type = undefined. It’s… undefined. Here, you’re dereferencing an undefined pointer. Of course it’ll segfault.

1 Like

To rephrase what others have already said, to get default initialization you need to assign to the whole value a struct literal.

const foo: Foo = .{};
const bar = Foo {};
const baz: *Foo = gpa.create(...);
baz.* = .{};

Without that, you don’t get default initialization because in Zig allocation is not initialization.

2 Likes

You are only setting the .value field, as others have pointed out allocation is not initialization.

Here is a way to initialize it setting the whole struct to useful values, that gives you an error if you miss any required fields:

var heapNode: *Node = try alocator.create(Node);
defer alocator.destroy(heapNode);
heapNode.* = .{ .value = 1 };
printNodes(heapNode);
2 Likes

Thank you everyone for your help.

I actually read that doc about allocation is not initialization when it was posted, but I totally forgot about it, maybe I wasn’t paying attention or didn’t try to practice it actually writing zig code.

I should try to write more zig code to actually learn zig. I only keep reading the posts and articles, but never writing any.

My favorite way of doing initialization is to have 2 “constructors”, one for stack allocation and the other one for heap allocation. Using your linked list example it would be something like this:

const std = @import("std");
const Allocator = std.mem.Allocator;

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

    fn init(val: i32) Node {
        return .{.value = val};
    }

    fn create(val: i32, a: Allocator) !*Node {
        const np = try a.create(Node);
        np.* = init(val);;
        return np;
    }
};

fn printNodes(n: *const Node) void {
    var maybe_node: ?*const Node = n;
    while (maybe_node) |node| {
        std.debug.print("node.value {d}\n", .{node.value});
        std.debug.print("node 0x{x:0>24}\n", .{@as(u96, @bitCast(node.*))});
        maybe_node = node.next;
    }
}

pub fn main() !void {

    const stackNode =  Node.init(1);
    printNodes(&stackNode);

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    var allocator = gpa.allocator();

    const heapNode = try Node.create(2, allocator);
    defer allocator.destroy(heapNode);
    printNodes(heapNode);
}
4 Likes