2

I've been trying for a few days now to create structs and then organize their pointers into a 2D array, with no luck. I then tried to re-create my issue in a small test with a 1D array, and I did recreate it. Why cannot I not use an array of pointers outside the scope it was created in? After I leave the scope, all the array indexes point to the last element added to the array. I've been trying lots and lots of different syntax, so if this is goofy, I've probably tried 20 other different ways. I've also tried in 0.10 and 0.11-preview-3312

I feel like this question is similar to another one, but that one doesn't seem to have an answer other than "just hardcode your array initialization" (How to create 2d arrays of containers in zig?)

Does anyone know how to put pointers into an array (compile time length is ok) and then pass the array to a different scope and access all the pointers?

const std = @import("std");
var arr: ?[3]*Foo = [3]*Foo{ undefined, undefined, undefined };

fn range(len: usize) []const void {
    return @as([*]void, undefined)[0..len];
}
// const allocator = std.heap.c_allocator;
test "Allocation stays after scope" {
    std.debug.print("\n", .{});
    if (arr) | *arra | {
        for (range(3)) |_, x| {
            arra.*[x] = &Foo{ .x = x };
            // arra.*[x] = @intToPtr(*Foo, @ptrToInt(&Foo{ .x = x }));
            // arra.*[x] = @intToPtr(*Foo, @ptrToInt(&Foo.init( 12*x )));
            std.debug.print("idx {}, {?}\n", .{x, arra.*[x]});
        }
    }
    if (arr) | *arra | {
        for (range(3)) |_, x| {
            std.debug.print("idx {}, {?}\n", .{x, arra.*[x]});
        }
    }
}

pub const Foo = struct {
    const Self = @This();
    x: usize,
    pub fn init(x:usize) Self {
        return Self {
            .x = x
        };
    }
};

The output is

idx 0, arr.Foo{ .x = 0 }
idx 1, arr.Foo{ .x = 1 }
idx 2, arr.Foo{ .x = 2 }
idx 0, arr.Foo{ .x = 2 }
idx 1, arr.Foo{ .x = 2 }
idx 2, arr.Foo{ .x = 2 }
Ken White
  • 123,280
  • 14
  • 225
  • 444
chugadie
  • 2,786
  • 1
  • 24
  • 33
  • 1
    You're making very similar mistake to this question: https://stackoverflow.com/q/76342370/944911. – sigod Jun 03 '23 at 14:07
  • is it because there's only 1 Foo{ .x = x } ever constructed and the address-of is always the same? if I do var foo = Foo{ .x = x } and then assign into the array &foo, I think I could reason that as being only 1 var in the loop. Is it because I'm not using an allocator to keep track of created Foos? – chugadie Jun 03 '23 at 16:20

1 Answers1

2

There is a lot going on in the posted code that might obscure the issue. Here is a much simplified version written for Zig v0.10.1:

const std = @import("std");

const Foo = struct {
    x: usize
};

pub fn main() void {
    var my_foo_ptrs: [3]*Foo = undefined;

    var i: usize = 0;
    while (i < 3) : (i += 1) {
        my_foo_ptrs[i] = &Foo{ .x = i };  // set ith pointer to local result location
    }

    // Print contents of array of structs using pointers.
    i = 0;
    while (i < 3) : (i += 1) {
        std.debug.print("{} ", .{ my_foo_ptrs[i].x });
    }
    std.debug.print("\n", .{});
}

Here is the output of the above program:

$ ./pointer_array 
2 2 2 

The OP expectation is that the array should contain 0 1 2, but here it seems that the array contains 2 2 2. The problem is in the line my_foo_ptrs[i] = &Foo{ .x = i };. Here the struct literal creates storage for a struct value locally, but this storage is not valid outside of the body of the block that contains it.

The Zig documentation seems to describe this process as instantiating a result location for the struct literal, but there is very little discussion of this in the docs. In any case, the principle should be familiar to anyone who has spent time in C or similar languages with no garbage collection: storage for local objects only exists locally. As far as I can tell, the behavior of the above program is undefined. It is not guaranteed that each evaluation of &Foo{ .x = i } will result in the same address, and it is not guaranteed that the storage referenced by these addresses will be in any way valid upon exit from the block.

Note that the current master version (Zig v0.11.0-dev.3363) will not compile this code. Previously, the type of &Foo{ .x = 1 } was *Foo, but in the current master version this type is *const Foo which is not compatible with *Foo in the assignment.

The solution is to create storage for the Foo structs that are referenced by the pointers, and to be aware of the lifetime of that storage. One simple way to do this is to create an array of Foo structs in main. This program works both in v0.10.1 and in the current master version:

pub fn main() void {
    var my_foos: [3]Foo = undefined;
    var my_foo_ptrs: [3]*Foo = undefined;

    var i: usize = 0;
    while (i < 3) : (i += 1) {
        my_foos[i].x = i;              // initialize the ith struct
        my_foo_ptrs[i] = &my_foos[i];  // initialize the ith pointer
    }

    // Print contents of array of structs.
    i = 0;
    while (i < 3) : (i += 1) {
        std.debug.print("{} ", .{ my_foos[i].x });
    }
    std.debug.print("\n", .{});

    // Print contents of array of structs using pointers.
    i = 0;
    while (i < 3) : (i += 1) {
        std.debug.print("{} ", .{ my_foo_ptrs[i].x });
    }
    std.debug.print("\n", .{});
}

The storage for the structs is no longer created in a loop block; instead it is created in an outer scope and the loop sets the pointers to reference these storage locations. After leaving the loop block, that storage still exists.

Here is the output of the above program:

$ ./pointer_array 
0 1 2 
0 1 2

Incidentally, with Zig v0.11.0 for loops have gained a range syntax that can make the above loops nicer, e.g.:

for (0..3) |i| {
    my_foos[i].x = i;              // initialize the ith struct
    my_foo_ptrs[i] = &my_foos[i];  // initialize the ith pointer
}
ad absurdum
  • 19,498
  • 5
  • 37
  • 60
  • thank you for the in-depth answer. I am not very familiar with C and non GC languages. It was very difficult to even express what the problem was to search for it. I guess my familiarity with reference counting would mean that the struct was still "reachable" somehow if I held a reference to it, but it's making more sense now. – chugadie Jun 04 '23 at 01:52