Proposal: Bounds-checking indexing

I frequently wish there was a way to combine bounds checking with indexing. What if you could index slices with slice?[index], which would check the bounds for you and return null if the index is out of range, instead of panicking/causing UB?

const foo = slice?[index];
// would be equivalent to
const foo = if (index >= 0 and index < slice.len) slice[index] else null;

There are lots of places in the stdlib that would benefit from this too, for example:

const src_digit_next = if (src_i + 1 < a.len) a[src_i + 1] else 0;
// would become:
const src_digit_next = a?[src_i + 1] orelse 0;

It’s much quicker to read. And since you don’t have to write the index twice anymore, there’s no more risk of them getting out of sync.

3 Likes

I don’t think this is common enough to warrant a dedicated language feature. Why not just make a function?

3 Likes

I didn’t think this is a bad idea, actually. Maybe the syntax could be worked on, (maybe reusing try?) but I think it would definitely be useful for more explicit runtime bounds-checking behavior.

I concur, I think what most people may agree upon is (conditionally) enabling bounds checking in debug builds, and optimize them away in release builds.

Writing a function which gets an element with bounds checking is not terribly hard, if you really really want it:

$ cat bounds.zig
const std = @import("std");

const GetError = error{OutOfBounds};

fn get(slice: anytype, i: usize) GetError!std.meta.Elem(@TypeOf(slice)) {
    return if (i >= 0 and i < slice.len) slice[i] else return error.OutOfBounds;
}

pub fn main() !void {
    const s: []const u8 = &.{ 0, 1, 2, 3, 4, 5 };
    for (0..s.len + 1) |i| {
        std.debug.print("get(s, {}) = {}\n", .{ i, get(s, i) catch 0xff });
    }
}

$ zig run bounds.zig
get(s, 0) = 0
get(s, 1) = 1
get(s, 2) = 2
get(s, 3) = 3
get(s, 4) = 4
get(s, 5) = 5
get(s, 6) = 255

Assuming index is usize there is no need for index >= 0 and :

const foo = if (index < slice.len) slice[index] else null;

I think this would encourage re-checking values, that are already known to be null, repeatedly, just because the operator makes it convenient.
Instead I would prefer early-exit (instead of chaining null through multiple later statements):

if(index >= slice.len) return null;
...
return something(slice[index]);
1 Like