Avoiding duplication of code for readonly or mutable access

I am having some trouble creating functions returning const or mutable stuff without duplicating code. This affects the way to write iterators as well.
Let’s take a simplified image example.
Sometimes I want readonly access, sometimesj mutable access.
How to handle this?
And while writing this thread I was wondering about the 3d function. Is that legal, the image being const and returning a mutable slice?

pub const Image = struct {
    width: u32,
    height: u32,
    pixels: []Pixel,

    // readonly
    pub fn get_line_slice_1(self: *const Image, y: u32) []const Pixel {
        const offset: u32 = self.width * y;
        return &self.pixels[offset..offset + width];
    }

    // mutable
    pub fn get_line_slice_2(self: *Image, y: u32) []Pixel {
        const offset: u32 = self.width * y;
        return &self.pixels[offset..offset + width];
    }

    // what about this??
    pub fn get_line_slice_3(self: *const Image, y: u32) []Pixel {
        const offset: u32 = self.width * y;
        return &self.pixels[offset..offset + width];
    }

}
1 Like

In this case, we only need get_line_slice_3 because the pixels field is just a slice (pointer).

*const Image only prevents modifying the Image object itself through that pointer. Constness is not transitive through the pointer stored in the pixels field, so it is perfectly valid for get_line_slice_3 to return a mutable []Pixel .

Of course, if pixels were an array instead of a slice, things would get a lot trickier.

3 Likes

But that’s why it’s an issue. For array field just a little meta programming on the input constness is enough. Here the problem is how should I encode constness for my “slice wrapper” type.

Zig has power here that library authors don’t have, because []u8 silently casts to []const u8, while MyMutSlice wont cast to MyConstSlice

FWIW here is what I ended up doing zml/zml/slice.zig at 053577d82d702225499d4717b25af3c584608cce · zml/zml · GitHub

Because here, mutable should be determined at compile time and not change at runtime, so encoding it at runtime isn’t really appropriate.

pub const slice = struct {
    pub fn Wrapper(comptime mutable: bool) type {
        return struct {
            bytes: if (mutable) []u8 else []const u8,
            shape: Shape,
            offset_bytes: usize = 0,
            byte_strides: stdx.BoundedArray(i64, constants.MAX_RANK),
            pub fn data(self: @This()) if (mutable) []u8 else []const u8 {
                return self.bytes;
            }
        };
    }
    pub fn initWrapper(shape: Shape, bytes: anytype) Wrapper(!@typeInfo(@typeOf(bytes)).pointer.is_const) {
        return .{
            .bytes = bytes,
            .shape = shape,
            .offset_bytes = 0,
            .byte_strides = shape.computeByteStrides(),
        };
    }
    ...
};

I get it but it makes all API really awkward because of the absence of conversion between const/mut variant

I know that this is just an example, but the correct answer here is don’t use a function when a field access suffices.

2 Likes

The way I usually handle this is by only writing the const version of the function. Then, you can handle the mutable case by using @constCast at the callsite. The rule here is that if you pass in mutable data, then you’re allowed to use @constCast on whatever you get back.

I am going to check on this one… This seems the most logical to me.

I tried your approach at some point, but in the end I decided it wasn’t worth the hassle. In practice const/non constness is relatively easy to track down, compared to eg ownership where the type system don’t help. We already have type safety to track host memory vs accelerator memory, so I didn’t want to multiply the nber of combintions.

1 Like

I recall that in stdlib they are doing some sort of dependent types on Slice.

If Slice is const then result is const, sort of that