Consider that a slice is simply a pointer and a length. There’s not really anything that special about it besides syntax. If two dimensional slices are a thing that you want, you can just code them. Eg:
const std = @import("std");
fn DoubleSlice(constness: enum { @"var", @"const" }, T: type) type {
const Pointer = switch (constness) {
.@"var" => [*]T,
.@"const" => [*]const T,
};
const Slice = switch (constness) {
.@"var" => []T,
.@"const" => []const T,
};
return struct {
ptr: Pointer,
size: [2]usize,
fn get(ds: *const @This(), a: usize, b: usize) T {
std.debug.assert(a < ds.size[0]);
std.debug.assert(b < ds.size[1]);
return ds.ptr[a * ds.size[0] + b];
}
fn getPtr(ds: *const @This(), a: usize, b: usize) *T {
std.debug.assert(a < ds.size[0]);
std.debug.assert(b < ds.size[1]);
return &ds.ptr[a * ds.size[0] + b];
}
// arr should be a pointer to double array, or slice of arrays.
fn init(arr: anytype) @This() {
return .{
.ptr = @ptrCast(arr.ptr),
.size = .{ arr.len, if (arr.len > 0) arr[0].len else 0 },
};
}
fn rowIter(ds: *const @This()) Iterator {
return .{
.ptr = ds.ptr,
.rows = ds.size[0],
.row_length = ds.size[1],
};
}
const Iterator = struct {
ptr: Pointer,
rows: usize,
row_length: usize,
fn next(iter: *Iterator) ?Slice {
if (iter.rows == 0) {
return null;
}
const r = iter.ptr[0..iter.row_length];
iter.ptr += iter.row_length;
iter.rows -= 1;
return r;
}
};
};
}
fn sum(ds: DoubleSlice(.@"const", u32)) u32 {
var iter = ds.rowIter();
var r: u32 = 0;
while (iter.next()) |row| {
for (row) |cell| {
r += cell;
}
}
return r;
}
pub fn main() void {
{
const arr: [2][2]u32 = .{
.{ 0, 1 },
.{ 2, 3 },
};
const s = sum(.init(&arr));
std.debug.print("2by2 sum: {d}\n", .{s});
}
{
const slice: []const [4]u32 = &.{
.{ 0, 1, 2, 3 },
.{ 4, 5, 6, 7 },
.{ 8, 9, 10, 11 },
};
const s = sum(.init(slice));
std.debug.print("3by4 sum: {d}\n", .{s});
}
}