The spec says the size of an anyopaque can't be zero. What is the reason?

The spec says:

void is distinct from anyopaque. void has a known size of 0 bytes, and anyopaque has an unknown, but non-zero, size.

So the following code is invalid, right?

const std = @import("std");

const T = struct {
    fn foo(_: *const T) void {
        std.log.info("foo", .{});
    }
};


pub fn main() !void {
    const t = &T{};
    const o: *const anyopaque = t;
    const t2: *const T = @ptrCast(@alignCast(o));
    t2.foo();
}

Haven’t tested it, but I think the distinction here is that a zero value of anyopaque would imply a null pointer, and null pointers are explicitly prohibited by the spec. If you want null values, you need an optional. A zero size would mean a zero value, hence it’s not allowed.

1 Like

Going further, just abridging the code a bit:

pub fn main() void {
    const t = &void{};
    const o: *const anyopaque = t;
    const t2: *const void = @ptrCast(@alignCast(o));
    std.debug.print("{*}, {}\n", .{ t2, t2.* });
}

This compiles, but gives:

void@aaaaaaaaaaaaaaaa, void

(This is the output in both -ODebug and -OReleaseFast.)

So maybe not necessarily invalid, but probably not something you want to do in practice, unless you are properly handling the type and never reference the pointer; with that said though, this did not produce a panic in -ODebug, so it may be safe (I give no guarantees on that statement though). :wink:

Going even further:

Note that this is technically not a zero value - you’re taking the address of the location of the value at this LOC (here, the void{}, or the T{} in your example). This might be where the confusion lies. :slight_smile:

Additionally:

std.debug.print("{} {} {}\n", .{ @sizeOf(@TypeOf(t)), @sizeOf(@TypeOf(o)), @sizeOf(@TypeOf(t2)) });
8 8 8

So *void has a non-zero size, which makes sense since it’s a pointer. anyopaque is a type-erased pointer and must be of non-zero size, which is correct. Again, maybe not recommended to use with void, but correct.

I get confused more now. :smiley:

I just checked 0.14 std src and found there are cases similar to my example. So currently, I tend to think the spec is imprecise here.

If anything, the language that distinguishes anyopaque from void could probably be omitted, or changed to explain that it is more akin to void * in C terms. It’s a type-erased pointer, not a type-erased value.

1 Like

My understanding is *anyopaque, instead of anyopaque, is a type-erased pointer.

3 Likes

https://ziglang.org/documentation/0.15.1/#Primitive-Types

Type C Equivalent Description
anyopaque void Used for type-erased pointers.

This reads to me that to fully understand the purpose of anyopaque, you need to take into account both parts here; that anyopaque is analogous to void, yes, but then add the additional qualifier that it’s only supposed to be used in the context of pointers. Hence, it’s more equivalent to void * (*anyopaque <=> void *).

2 Likes

This is conjecture based on a few minutes of searching through old issues and commits, but I suspect that

anyopaque has an unknown, but non-zero, size

is a leftover from an older version of the language that had different semantics, and that it should more correctly be

anyopaque has an unknown size

(indirectly implying that the size can be 0).

In 0.8.x, pointers to zero-bit types were themselves also zero-bit types, i.e. @sizeOf(*void) == 0 and @sizeOf(*void) == @sizeOf(void). *c_void (the old name for *anyopaque) on the other hand was not a zero-bit type, i.e. @sizeOf(*c_void) != 0, and therefore it was important to clarify that a *c_void/*anyopaque payload is at least one byte in size.

Some time before 0.10.x, the “pointers to zero-bit types are zero-bit types” idea was scrapped because it made the language inconsistent and made it difficult to write generic code.

*void and pointers to OPV types (“One Possible Value”, a different name for zero-bit types) have addresses just like pointers to non-OPV types. The main distinction is that dereferencing a pointer to an OPV type is a no-op that always succeeds and never results in a memory access violation, and that you can’t use pointer arithmetic with pointers to OPV types like [*]u0.

9 Likes