Modifying slice information

I would like to understand modifying slice semantics.
Suppose I have a slice passed in whose information I’d like to modify – how would I go about doing so?

i.e.

pub fn shorten_len(comptime T: type, slice: []T) void {
    slice.len = 1;
    return void;
}

Would this modify the underlying heap allocation? Or are slices passed by value? How would I modify this slice information in place?

A slice is like a struct that contains a pointer and a len, both of which are just usize. The example code you wrote won’t work because you’re modifying a stack variable and immediately dropping it. I.E., try writing the same function with an integer:

// This won't compile because function args are immutable, but you get the idea
pub fn dec(len: usize) void {
    len -= 1;
    return void;
}

pub fn main() void {
    const foo: usize = 10;
    dec(foo);
    std.debut.print("{}", .{foo});
}

The value of foo stays the same because len is a stack variable, and is passed by value. If you want to modify the slice, you can either pass a reference to the slice or return the newly modified slice.

pub fn shorten(foo: *[]const u8) void {
    foo.len -= 1;
}

pub fn shorten(foo: []const u8) []const u8 {
    // Note: here the slice itself is mutable, but the data it's pointing to is immutable
    var shorter = foo;
    shorter.len -= 1;
    return shorter;
}

As for your other question, no, modifying the slice doesn’t modify the underlying allocation. That memory remains allocated until freed; if you allocate 100 bytes, then try to free a 99 byte slice, you get a memory leak.

It’s important to keep track of where all of your memory lives, how long it lives, and what part of the code “owns” it.

I’m on mobile so this may be messed up, I can fix it later

Thank you very much for your reply! I appreciate the thought that you’ve put into it. Although I’m afraid that it’s a little weird to pass in a pointer to a fat pointer, I assume that that is what I must do.
I will modify my implementation to do so – it is necessary because I’m writing an allocator, and thus I must be very careful about pointer semantics.
Once again, thank you for your kind reply, and I hope to have further conversation with you

That being said, however. How do I deal with situations like so:

    /// Attempt to expand or shrink memory in place.
    ///
    /// `memory.len` must equal the length requested from the most recent
    /// successful call to `alloc`, `resize`, or `remap`. `alignment` must
    /// equal the same value that was passed as the `alignment` parameter to
    /// the original `alloc` call.
    ///
    /// A result of `true` indicates the resize was successful and the
    /// allocation now has the same address but a size of `new_len`. `false`
    /// indicates the resize could not be completed without moving the
    /// allocation to a different address.
    ///
    /// `new_len` must be greater than zero.
    ///
    /// `ret_addr` is optionally provided as the first return address of the
    /// allocation call stack. If the value is `0` it means no return address
    /// has been provided.
    resize: *const fn (*anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) bool,

In implementing this vtable, it’s expected that I modify the slice in place, apparently. Is there anything I can do about this?

nope, the function tells the caller if the resize is successful or not, the caller is responsible for updating their knowledge of the allocation.

Alright then. Thank you very much!

One example where then length of the slice is directly changed in the standard library is in the ArrayList. Maybe this helps your understanding a bit more.

The semantics of a slice are similar to a structure like this:

fn Slice(comptime T:type) type {
    return struct {
        ptr: [*]T,
        len: usize,
    };
}

It’s essentially a pointer carrying length information. First, it possesses all the inherent complexities of a pointer. A pointer may or may not own a block of heap memory. The same applies to slices; they may correspond to a block of heap memory or simply be a reference to other heap or stack memory. Therefore, you must understand where a slice comes from before making a judgment.

Second, there’s the complexity introduced by the length information it carries. If your modifications to the slice don’t involve len, then the slice is stateless; you can freely modify its contents without changing len.

However, once you attempt to modify len, it means you’re using a stateful slice. Your modifications only affect the current slice, and other copies of the slice are unaware of your length modification.

If your slice is allocated on the heap, attempting to modify len means your slice itself cannot fully express heap memory ownership information. You must first store the maximum capacity of the slice in an additional form; in this case, ptr[0..capacity] is the true owner of this slice, while slices whose len can be modified only exist as references.

Therefore, we discuss stateful slices used as references: regardless of whether their ownership is on the stack or the heap, you must be mindful of their capacity. If the slice comes from a stack buffer with a certain capacity, you cannot modify its contents beyond the buffer’s size. If the slice comes from heap memory, and you want to modify its contents beyond its capacity, you must reallocate a larger block of heap memory.

In practice, I recommend the convention of treating slices as stateless, immutable, fat pointers. If you want to modify them dynamically(especially the need to modify len), always use std.ArrayList, and never use a naked slice.

5 Likes