Setting value to partial set of a nullable array

I have a program where I loop over a list and save some values to an array like this snippet.

fn f1() !void {
    const KV = struct {k: u32, v: f64};
    const list = &[_]KV{.{.k = 1, .v = 1}, .{.k = 10, .v = -1}};
    var arr: ?[10]u32 = null;
    var i: usize = 0;
    for (list) |a| {
        if (a.v > 0 and i < 10) {
            arr[i] = a.k;
            i += 1;
        }
    }
}

The problem is that I get error: type ‘?[10]u32’ does not support indexing . I think I have two options either 1. store a variable var found: bool = false and update it if I find any 2. keep ?[10]u32 and change the condition to this

if (a.v > 0 and i < 10) {
   if (arr == null)  {
      arr = .{0} ** 10;
   }
   arr.?[i] = a.k;
   i += 1;
}

I haven’t looked at the generated assembly but it feels like the compiler will not optimize the initialization of the array plus it doesn’t convey what I have in mind.

What do you guys suggest? Is there another options?

Why not use the index i that you already have for that purpose, if it’s zero at the end, then nothing was found.

2 Likes

I think this will do what you want.

fn f1() !void {
    const KV = struct {k: u32, v: f64};
    const list = &[_]KV{.{.k = 1, .v = 1}, .{.k = 10, .v = -1}};
    var found_buf: [list.len]u32 = undefined; // list.len is the maximum number of found values
    var found_list: std.ArrayList(u32) = .initBuffer(&found_buf);
    for (list) |a| {
        if (a.v > 0) {
            try found_list.appendBounded(a.k);
        }
    }
    // after, found_list.items is a slice that contains the a.k values where a.v > 0
}
1 Like

Yeah, both solutions sound great but I guess in my current situation @IntegratedQuantum solution is more appropriate. Thanks

One more tip: depending on what you’re doing, you may not have to do anything special for the ‘nothing found’ case.

If your code looks like this:

fn f1() !void {
    const KV = struct {k: u32, v: f64};
    const list = &[_]KV{.{.k = 1, .v = 1}, .{.k = 10, .v = -1}};
    var arr: [10]u32 = null;
    var i: usize = 0;
    for (list) |a| {
        if (a.v > 0 and i < 10) {
            arr[i] = a.k;
            i += 1;
        }
    }
    // Do stuff
}

Your next line is probably something like

    const found = arr[0..i];

Making a slice containing what you’ve found. If you didn’t find anything, the slice is empty.

So later, if you iterate over the slice:

    for (found) |kv| {
        doSomething(kv);
    }

If the slice has no contents, nothing happens.

This is the more convenient way to deal with a slice, almost always. If you define a ?[]u32, that is, maybe a slice, maybe null, you still have the case where the slice .len is 0, so that’s two ways of having no contents in the slice: the empty slice, and no slice at all. We only need one, and as shown above, it’s not always necessary to detect that case in order to do the correct thing.

1 Like
-    var arr: [10]u32 = null;
+    var arr: [10]u32 = undefined;

And I came to the same conclusion about what their next code would look like, which is why I recommended using a std.ArrayList to just build the slice directly. Under the hood, it’s doing the same thing as explicitly tracking i, it’s just a bit harder to make mistakes by accidentally getting i and arr out of sync.

1 Like

That would have the advantage of compiling, yes :wink:

The sample code contains an explicit limit on the upper bound, which is checked. An ArrayList would perform allocations, which is necessary in the absence of an upper bound, but in the presence of one? A stack array is cleaner, in my opinion.

Check my code again, I used .initBuffer(&stack_array) and .appendBounded(item), which do no allocations :slight_smile:

You could also use .appendAssumeCapacity(item) if you’re confident the length of &stack_array is enough. It asserts instead of possibly returning error.OutOfMemory.

If you do just want the first 10 items found, it could be changed to:

fn f1() !void {
    const KV = struct {k: u32, v: f64};
    const list = &[_]KV{.{.k = 1, .v = 1}, .{.k = 10, .v = -1}};
-    var found_buf: [list.len]u32 = undefined; // list.len is the maximum number of found values
+    var found_buf: [10]u32 = undefined;
    var found_list: std.ArrayList(u32) = .initBuffer(&found_buf);
    for (list) |a| {
        if (a.v > 0) {
-            try found_list.appendBounded(a.k);
+            found_list.appendBounded(a.k) catch break;
        }
    }
    // after, found_list.items is a slice that contains the a.k values where a.v > 0
}

or better yet:

fn f1() !void {
    const KV = struct {k: u32, v: f64};
    const list = &[_]KV{.{.k = 1, .v = 1}, .{.k = 10, .v = -1}};
-    var found_buf: [list.len]u32 = undefined; // list.len is the maximum number of found values
+    var found_buf: [10]u32 = undefined;
    var found_list: std.ArrayList(u32) = .initBuffer(&found_buf);
    for (list) |a| {
+       if (found_list.items.len == found_buf.len) break;
        if (a.v > 0) {
-            try found_list.appendBounded(a.k);
+            found_list.appendAssumeCapacity(a.k);
        }
    }
    // after, found_list.items is a slice that contains the a.k values where a.v > 0
}

You can also use a slice pointer in this situation:

    var buffer: [10]u32 = undefined;
    var arr: []u32 = buffer[0..0];
    for (list) |a| {
        if (a.v > 0 and i < 10) {
            arr.len += 1;
            arr[arr.len - 1] = a.k;
        }
    }
}

arr will be empty if nothing was found.

I’m pretty sure that’s unsound.

IIRC

var thing = buffer[0..0];

leaves thing.ptr undefined;

Are you sure? I thought that was how ArrayList works.

EDIT: this is how ArrayList.initBuffer works.

1 Like

No, I’m not, I was more confident before reading Zig Documentation ^1. but I know I’ve used const thing = slice[0..runtime_len_of_zero]; before, which results in a segv when passed to linux writev(). because thing.ptr was left undefined;

^1:

        /// Initialize with externally-managed memory. The buffer determines the
        /// capacity, and the length is set to zero.
        ///
        /// When initialized this way, all functions that accept an Allocator
        /// argument cause illegal behavior.
        pub fn initBuffer(buffer: Slice) Self {
            return .{
                .items = buffer[0..0],
                .capacity = buffer.len,
            };
        }

This seems to pass a zig test at least.

const testing = @import("std").testing;

test "segfault on zero length slice" {
    var array = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
    const slice_comptime: []const u8 = array[0..0];
    var runtime_zero: usize = 0;
    _ = &runtime_zero;

    const slice_runtime: []const u8 = array[0..runtime_zero];

    try testing.expect(@intFromPtr(slice_comptime.ptr) != 0);
    try testing.expect(@intFromPtr(slice_runtime.ptr) != 0);
}

And running further tests,

const std = @import("std");
const testing = std.testing;

pub fn main() !void {
    var array = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
    var runtime_zero: usize = 0;
    _ = &runtime_zero;

    const slice_runtime: []const u8 = array[0..runtime_zero];

    std.log.info("{*}", .{slice_runtime.ptr});
}

also produce working results:

$ zig run compiler_bug_ptr_zero.zig 
info: u8@7ffe2a06bd58

EDIT: apologies for the deleted comment below, I became out of sync between what I was copying/pasting between VIM and the ziggit.dev editor and at some point must have clicked the reply rather than edit button.

(post deleted by author)