`deinit` `.empty` MultiArrayList

Hello, I was wondering how MultiArrayList.deinit interacts with a MultiArrayList obtained with .empty? I see empty is defined as follows:

pub const empty: Self = .{
    .bytes = undefined,
    .len = 0,
    .capacity = 0,
};

… and deinit as follows:

/// <SNIP>
fn allocatedBytes(self: Self) []align(sizes.big_align.toByteUnits()) u8 {
    return @alignCast(self.bytes[0..capacityInBytes(self.capacity)]);
}

pub fn deinit(self: *Self, gpa: Allocator) void {
    gpa.free(self.allocatedBytes());
    self.* = undefined;
}

Won’t this result in Illegal Behaviour if deinit is called on a MultiArrayList with an undefined bytes field (from reading uninitialised memory)?

This is valid behavior, allocators have an early return in free before it ever uses or accesses the pointer value for anything:

/// Free an array allocated with `alloc`.
/// If memory has length 0, free is a no-op.
/// To free a single item, see `destroy`.
pub fn free(self: Allocator, memory: anytype) void {
    const slice_info = @typeInfo(@TypeOf(memory)).pointer;
    comptime assert(slice_info.size == .slice);
    const bytes: []u8 = @ptrCast(@constCast(mem.absorbSentinel(memory)));
    if (bytes.len == 0) return;
    @memset(bytes, undefined);
    self.rawFree(bytes, .fromByteUnits(slice_info.alignment orelse @alignOf(slice_info.child)), @returnAddress());
}

Rule of thumb: It’s always legal to free 0-sized memory even if it was never allocated with the allocator.
You’ll see this pattern a lot throughout the std data structures.
Analogously alloc of 0-bytes also has an early return.

3 Likes

Thanks for the quick reply. I’d seen that in the documentation, but won’t the slice expression access the uninitialised field? I guess it might not be illegal to simply access an uninitialised field (in this case the pointer)?

I assume you mean self.bytes[0..capacityInBytes(self.capacity)]?
Slicing will never actually access the underlying data. You can think of a slice as a

struct { ptr: [*]T, len: usize }

And what slicing does is basically just pointer arithmetic, at no point does it access the pointer value:

y = x[a..b];
// is roughly equivalent to
assert(a <= b and b <= x.len);
y.ptr = x.ptr + a;
y.len = b - a;

And doing arithmetic like addition on undefined values is totally fine. It only becomes problematic if it generates side effect (e.g. a branch or a dereference an access to the undefined address).

2 Likes

Right, but isn’t the field itself uninitialised? That is, mightn’t self.bytes just be some garbage value? If so, isn’t it illegal to read the field until it’s assigned a valid value (even if that pointer is never read through)? E.g.

const std = @import("std");

pub fn main() void {
    var buffer: [1024]u8 = [_]u8{0} ** 1024;
    var x: [*]u8 = undefined;

    std.debug.print("{*}\n", .{x});

    x = &buffer;

    std.debug.print("{*}\n", .{x});
}

Doesn’t this program exhibit illegal behaviour on line 7 (before x = ...), even though it doesn’t dereference the pointer?

No it is perfectly valid to overwrite an undefined value,

In fact It’s necessary to do so before you try to use it as if it were a valid value.

Oops, sorry, you were asking about reading the address. I am not sure if that is legal or not TBH, but at the very least it doesn’t do any harm in this case.
But it could do harm if you used the value as if it were valid, that would certainly be illegal behaviour.

Actually, if the following is not illegal, then reading the address alone cannot be illegal as that is involved with the following.

I think you meant to ask if x + n would be illegal since x is a pointer with an undefined address, but no it is still legal.

What would be illegal would be to dereference x before it is assigned a valid address.

I see. Why is reading the address not illegal? I assumed undefined was similar to the [[indeterminate]] attribute in C++ 26, or MaybeUninit in Rust, both of which allow skipping the initialisation of variables for performance, but require initialisation before any reads. Is this a wrong assumption?

Yes it does, but the reason is not because it’s illegal to read the value of x, but because you are using the value of x in a way that causes side effects, in this case through printing.
In case of MultiArrayList it is doing arithmetic, but the result is never used in a way that can cause side effects.


At compile-time Zig has already implemented safety checks for illegal use of undefined behavior.
To give you another example, consider the following:

comptime var x: u64 = undefined;
x += 1;

This causes a compiler error

test.zig:36:2: error: use of undefined value here causes illegal behavior
 x += 1;
 ^

You may think it’s because x is being read, but in reality it’s because += can panic on overflows, since the value is undefined, both outcomes (panicing or not) are possible, which is illegal.

To prove this point, change the operator to +%= or +|= which never panic, and you’ll see it does compile:

comptime var x: u64 = undefined;
x +%= 1;

So in conclusion reading and doing simple arithmetic on undefined is allowed, as long as there are no other side effects.

5 Likes

I guess it would be more accurate to say that it doesnt matter if its illegal or not, because the value has no meaningful effect on the program.

But at this point i am speculating, which is not useful to either of us.

At least in the de allocation that started this topic; as @IntegratedQuantum points out, the print example is effected by the value so is illegal.

1 Like

I see, so because the slice is never used (for side effects), reading self.bytes is not illegal, despite being uninitialised?

Right.