Can I assert that a value is undefined?

If I do ptr.* = value;, can I somehow assert (eg, get a trap in ReleaseSafe) that the original value of ptr was undefined? I can manually use an optional type, but that wouldn’t be optimal.

Optional pointers cost nothing, null for them is just 0, as in C, citation:

Instead, you can use an optional pointer. This secretly compiles down to a normal pointer, since we know we can use 0 as the null value for the optional type. But the compiler can check your work and make sure you don’t assign null to something that can’t be null

It won’t compile.

const std = @import("std");

pub fn main() void {
    const ptr1: *u32 = undefined;
    const ptr2: *u32 = undefined;
    std.debug.print("ptr1 = {}, ptr2 = {}\n", .{ptr1, ptr2});
    ptr1.* = 1;
    ptr2.* = 2;
    std.debug.print("*ptr1 = {}, *ptr2 = {}\n", .{ptr1.*, ptr2.*});
}

and then

$ /opt/zig-0.10.1/zig build-exe p.zig 
p.zig:8:9: error: cannot dereference undefined value
    ptr1.* = 1;
    ~~~~^~

I don’t think you can. :frowning:

I’m curious, not optimal in what way?

I think you’re making a slightly different example here: an undefined pointer value, not a pointer to an undefined value. A closer example would be:

const std = @import("std");

fn overwrite(ptr1: *u32, ptr2: *u32) void {
    ptr1.* = 1;
    ptr2.* = 2;
}

pub fn main() void {
    var ptr1: u32 = undefined;
    var ptr2: u32 = undefined;
    std.debug.print("ptr1 = {}, ptr2 = {}\n", .{ ptr1, ptr2 });
    overwrite(&ptr1, &ptr2);
    std.debug.print("ptr1 = {}, ptr2 = {}\n", .{ ptr1, ptr2 });
}

Which compiles just fine.

1 Like

So, I want to write code like this:

struct {
    callback: *const fn() = undefined,

    fn arm(self: *@This(), f: *const fn()) void {
        assert(self.callback == undefined);
        self.callback = f;
    }

    fn disarm(self: *@This(), f: *const fn()) void {
        assert(self.callback != undefined);
        self.callback = undefined;
    }
}

with the invariant that arm and disarm must be called in sequence.

I can write it like this instead:

struct {
    callback: ?*const fn() = null,

    fn arm(self: *@This(), f: *const fn()) void {
        assert(self.callback == null);
        self.callback = f;
    }

    fn disarm(self: *@This(), f: *const fn()) void {
        assert(self.callback != null);
        self.callback = null;
    }
}

But then even in ReleaseFast mode there’s an extra flag in memory, and also each call-site would have to do self.callback.? which is extra syntactic noise that makes it unclear that it really an intentional invariant, rather than sloppy optional handling.

Are you sure? null value for an optional pointer is just 0, without any extra flag in Zig.

const std = @import("std");
const print = std.debug.print;

pub fn main() void {
    var _u64: u64 = 0;
    var opt_u64: ?u64 = null;
    var _ptr: *u64 = undefined;
    var opt_ptr: ?*u64 = null;
    print("{}\n", .{@sizeOf(@TypeOf(_u64))}); 
    print("{}\n", .{@sizeOf(@TypeOf(opt_u64))}); 
    print("{}\n", .{@sizeOf(@TypeOf(_ptr))}); 
    print("{}\n", .{@sizeOf(@TypeOf(opt_ptr))}); 
}

ouput

8
16 // suprise!
8
8

But, despite the ‘suprise’ (what the fuck?..) , the size of an optional pointer is the same as the size of ‘mandatory’ pointer

const std = @import("std");
const print = std.debug.print;

pub fn main() void {
    var _u16: u16 = 0;
    var opt_u16: ?u16 = null;
    var _u32: u32 = 0;
    var opt_u32: ?u32 = null;
    var _u64: u64 = 0;
    var opt_u64: ?u64 = null;
    print("    u16 : {}\n", .{@sizeOf(@TypeOf(_u16))}); 
    print("opt u16 : {}\n", .{@sizeOf(@TypeOf(opt_u16))}); 
    print("    u32 : {}\n", .{@sizeOf(@TypeOf(_u32))}); 
    print("opt u32 : {}\n", .{@sizeOf(@TypeOf(opt_u32))}); 
    print("    u64 : {}\n", .{@sizeOf(@TypeOf(_u64))}); 
    print("opt u64 : {}\n", .{@sizeOf(@TypeOf(opt_u64))}); 
}
    u16 : 2
opt u16 : 4
    u32 : 4
opt u32 : 8
    u64 : 8
opt u64 : 16

So, the size of the ‘flag’ is the same, as for ‘base’ type.
But not for pointers.

If you want an optional method/callback, just make it optional and call it if it is null.
If that callback can not be null by design just assert it is not null.

No, @matklad did mean undefined pointer value (not the undefined value of the pointtee),
and, as you can easily see, the size of an optional pointer is… just the size of a pointer,
unlike other optionals… which (sizes of them) are somewhat strange, probably this is for alignment reasons, I do not know tbh.

As others have said, optional pointers don’t occupy any extra memory vs regular pointers, in case that’s what you were referring to when you said “extra flag in memory”. The .? operator can indeed be used both ways, but it’s intended use is for asserting that something is not null in the UB sense of the term, so it would be appropriate to use it that way.

Zig in general doesn’t have all the type tooling required to ensure that type of thing. What you can do is have arm return a type that has disarm in it, so that you only get access to the second function from the result of calling the first (and vice versa).

2 Likes