Trying to use a two dimensional array as a a two dimensional slice

Hi,

I am trying to pass a two dimensional array [140][140]u8 into a function that accepts a two dimensional [][]u8slice. This does not work. But it works if I change the function to accept a slice of arrays [][140]u8slice. I would like my function to not be dependent on the size.

Sorry if this is a trivial question (I did not manage to find an answer online) but I would be grateful to get an explanation for this behavior. It seems strange to me that the conversion does not work for multidimensional arrays/slices since all the array sizes are available at the compile time.

var input: [140][140]u8 = undefined;

fn modifyTwoDimSlice(in: [][]u8) void {
    // fn modifyTwoDimSlice(in: [][140]u8) void { // ← this works
    in[0][1] = 255;
}

pub fn main() !void {
    modifyTwoDimSlice(&input);
}

Gives the error:
-----------------------
two_dim_slice_question.zig:8:23: error: expected type '[][]u8', found '*[140][140]u8'
    modifyTwoDimSlice(&input);
                      ^~~~~~
two_dim_slice_question.zig:8:23: note: pointer type child '[140]u8' cannot cast into pointer type child '[]u8'
two_dim_slice_question.zig:3:26: note: parameter type declared here
fn modifyTwoDimSlice(in: [][]u8) void {
                         ^~~~~~
-----------------------
1 Like

This is because the rows (the inner arrays) may or not be a plain array of bytes. your input is represented as a 140 * 140 u8 in memory, but the arguments expects a slice (with pointer + size) of slices (more pointer and slices).

I suggest you to wrap multidimensional arrays in a struct with memory slice and width + height. Using an array of array will only cause more pain in my experience. Then use x + * width for indexing

6 Likes

I think it is not what you want, but …

var input: [140][140]u8 = undefined;

fn modifyTwoDimSlice(comptime i: usize, comptime j:usize, in: *[i][j]u8) void {
    // fn modifyTwoDimSlice(in: [ ][140]u8) void { // ← this works
    in[0][1] = 255;
}

pub fn main() !void {
    modifyTwoDimSlice(140, 140, &input);
}
2 Likes

This a bit more generic, may be what you want …

const std = @import("std");
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
pub const allocator = gpa.allocator();

var input: [][]u8 = undefined;

fn modifyTwoDimSlice(in: *[][]u8) void {
    (in.*)[0][1] = 255;
}

pub fn main() !void {
    const lines = 101;
    const columns = 77;
    input = try allocator.alloc([]u8, lines);
    for (0..input.len) |i| {
        input[i] = try allocator.alloc(u8, columns);
    }
    modifyTwoDimSlice(&input);
    std.debug.print("input[0,1]: {any}\n", .{input[0][1]});
}
1 Like

Thank you all for the good suggestions, it makes sense to me now.

Zig only allows conversion from array to slice for one dimmensional data, for multi dimmensional data supporting this would require Zig to implicitly create memory for the inner slices and Zig avoids hidden behavior or memory management.

So you have to create the memory for the inner slices yourself and then you can create a slice to the array of inner slices.

const std = @import("std");

fn modifyTwoDimSlice(in: [][]u8) void {
    // fn modifyTwoDimSlice(in: [][140]u8) void { // ← this works
    in[0][1] = 255;
}

pub fn main() !void {
    var input: [140][140]u8 = undefined;

    var inner: [140][]u8 = undefined;
    for (0..140) |i| inner[i] = &input[i];

    const outer_slice: [][]u8 = &inner;

    modifyTwoDimSlice(outer_slice);

    std.debug.print("value: {}\n", .{outer_slice[0][1]});
}

I agree with @Etienne_Parmentier that it is better to use a flat slice (if you can) with width and height information, because that doesn’t require all those inner slices and also potentially has much better cache locality.

4 Likes

Thank you for the clear explanation, it is nice to see how it is possible to make a two dimensional slice from an array. But as you say I, it is more efficient to use a flat slice.

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});
    }
}

1 Like

This is next level for me. Thank you, I will take my time to understand this.

If you don’t mind extra allocation of [][]T and want simplicity when using, you can try this:

const mdslice: [][]u8 = try allocator.alloc([]u8, 140);
for (mdslice) |*s| {
    s.* = try allocator.alloc(u8, 140);
}
defer {
    for (mdslice) |s| {
        allocator.free(s);
    }
    allocator.free(mdslice);
}

And another way that has a contiguous area of storage:

const storage = try allocator.alloc(u8, 140 * 140);
const mdslice: [][]u8 = try allocator.alloc([]u8, 140); // `mdslice` 's lifespan is dominated by `storage`
for (0..140) |i| {
    mdslice[i] = storage[140 * i .. 140 * (i + 1)];
}
defer allocator.free(storage);
defer allocator.free(mdslice);

But write them into a specific type. Result will be something as @ScottRedig provided.

Thank you, nice to see this version as well.

If the cardinalities of the array dimensions are known ahead of time, you can take advantage of Zig’s type system by creating the array types for the two-dimension arrays using a type constructor, like D2Array() in the code below. Then just pass the array around by pointer. This approach works with N-dimension beyond two.

fn D2Array(T: type, width: usize, height: usize) type {
    return [height][width]T;
}

const Array8x8 = D2Array(u8, 8, 8);
const Array140x140 = D2Array(u8, 140, 140);

fn modify8x8(array: *Array8x8) void {
    array[0][1] = 255;
    array[0][9] = 255;  // compile error caught by the compiler
}

fn modify140x140(array: *Array140x140) void {
    array[0][1] = 255;
}

fn modify(comptime W: usize, comptime H: usize, array: *D2Array(u8, W, H)) void {
    for (0..H)|h| {
        for (0..W)|w| {
            array[h][w] = 255;
        }
    }
}

test {
    var a1: Array8x8 = undefined;
    var a2: Array140x140 = undefined;

    modify8x8(&a1);
    modify140x140(&a2);
    modify(8, 8, &a1);
    modify(140, 140, &a2);
}

This is not exactly what you’re asking for. It’s just simpler to work with a single type of data instead of converting to slices and back. You get the benefit of the dimensions of the array being checked by Zig’s type system.

2 Likes

Thank you, nice to see this full example of using compile time. I really appreciate the good replies from you all.