How could this program work?

const std = @import("std");

const Foo = struct {
    a: u8,
};

fn set(foo: *Foo) void {
    foo.a = 2;
}

pub fn main() !void {
    const foo = Foo{ .a = 1 };
    set(@constCast(&foo));
    std.debug.print("{any}\n", .{foo});
}

This program will print Foo{ .a = 1 }, which doesn’t make any sense to me.

IMO, it should panic when running in debug mode, since it violates that foo is a const, and shouldn’t be changed.

1 Like

I guess this is because @constCast made a copy of the data and you just changed the copy. Add a std.debug.print("{any}\n", .{&foo}); after your print and you’ll see that &foo has the updated value.

Since foo is constant, compiler is free to replace uses of foo with it’s definition.

const foo = Foo{ .a = 1 };
std.debug.print("{any}\n", .{foo});

becomes:

const foo = Foo{ .a = 1 };
std.debug.print("{any}\n", .{Foo{ .a = 1 }});

So the result is correct.


Since the program gets the address of a constant, it must allocate its data in a read only area. If it did that foo.a = 2 should crash. (This is my opinion and obviously not the way that zig works).

I posted about this a while back; there are different opinions on whether this is a compiler bug or normal consequence of optimizations. Mutation after @constCast

1 Like

I’ll throw in something ala Scott Meyers here that helps clarify the use cases around this. I made a topic about using @constCast a while ago: Method overloading and deduplication using @constCast - #3 by AndrewCodeDev

I’ve linked to the post with this example:

fn MatchPtrType(comptime Parent: type, comptime Child: type) type {
    return if (comptime isConstPtr(Parent)) *const Child else *Child;
}

// @constCast can propgate through optional pointers... ?*const T -> ?*T

pub fn last(self: anytype) ?MatchPtrType(@TypeOf(self), Node) {
    if (comptime isConstPtr(@TypeOf(self))) {
         
        return if (self.items.len > 0)
            &self.items[self.items.len - 1] else null;
         
    } else {
        return @constCast(asConst(@This(), self).last());
    }
}  

The pattern that works is:

non-const x -> *const x -> @constCast(ptr)

You do not want to go in this direction:

const x -> *const x -> @constCast(ptr)

The reason being is that the compiler has made assumptions about x because it begins as const. You’re invalidating those assumptions. If you start with non-const, go up to const, and then back down, everything is fine.

  • Edit -

I should probably make a Doc on this…

2 Likes

I think the answer is that this is just UB. With @constCast the programmer asserted that the “thing” being pointed to by the pointer can be written, but then this was not true and so optimizations like what dimdin mentioned become observable (and wrong) etc.

3 Likes