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?
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
}
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.
- 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.
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
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
}
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,
};
}
$ 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.