Const *S versus var *S

referring to this post, here’s a simple program with both sorts of pointer to a mutable struct instance:

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

const S = struct {
    x: u32 = 10,
};

var vp: *S = @constCast(&std.mem.zeroInit(S, .{}));
const cp: *S = @constCast(&std.mem.zeroInit(S, .{}));

pub fn main() void {
    // no errors
    vp.x = 20;
    print("vp.x = {d}", .{vp.x});
    // no compile-time error
    cp.x = 20;
    // runtime crash
    print("cp.x = {d}", .{cp.x});
}

i would have thought (hoped??) that i could modify cp.x – but apparently this crashes at runtime…

FWIW, i also tried using cp.*.x but with the same results…

this is failing in 0.13.0… in 0.12.0, there’s no failure but the print output shows 10…

this problem is somewhat related to an earlier post of mine… i was hoping to solve this problem by having const-pointers-mutable-data within my outer comptime struct…

but then i realized some more fundamental isn’t working as i thought…

So @biosbob, I’ve noticed that you’re using the following idiom to create anonymous struct literals quite a lot:

std.mem.zeroInit(Foo, .{})

And you’re casting away const to create mutable pointers of these immutable objects. I honestly cannot recommend this pattern. I think you’re going to run into a horde of bugs… big nasty bugs with pincers and fangs that are armed with guns specifically designed for feet.

I’m curious why you’re not opting for something simpler where you are expressing that the thing itself is non-const and can change instead of making something that is assumed to be const that is viewed as non-const?

Something like…

const std = @import("std");
const print = std.log.info;

const S = struct {
    x: usize = 0,
};

pub fn makeUnique(comptime _: anytype) *S {
    // you can still use zeroInit here...
    return &struct { var s: S = std.mem.zeroInit(S, .{}); }.s;
}

var vp: *S = makeUnique(opaque{});
const cp: *S = makeUnique(opaque{});

pub fn main() void {
    // no errors
    vp.x = 20;
    print("vp.x = {d}", .{vp.x});
    // no compile-time error
    cp.x = 20;
    // runtime crash
    print("cp.x = {d}", .{cp.x});
}

I’m trying to understand the advantage here because it seems like (imo) you’re fighting an uphill battle using const casting but I am seeing you go back to this pattern quite frequently. Am I missing something here?

1 Like

@biosbob I’m seeing in your examples a lot of experimentation with constCast, just to make sure we’re on the same page, know that this is how to not crash nor require the use of constCast:

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

const S = struct {
    x: u32 = 10,
};

var _vp: S = std.mem.zeroInit(S, .{});
var vp: *S = &_vp;
var _cp: S = std.mem.zeroInit(S, .{});
const cp: *S = &_cp;

pub fn main() void {
    vp.x = 20;
    print("vp.x = {d}", .{vp.x});
    cp.x = 20;
    print("cp.x = {d}", .{cp.x});
}

If this was already clear to you, then an explanation of your goal would help me help you better.

i guess it’s a little strange that i need to use vars _vp and _cp as intermediaries – though it certainly makes intentions clearer to the reader as well as the compiler…

combining the suggestions from @AndrewCodeDev, i can keep my top-level namespace less polluted:

var vp: *S = &struct {
    var s = std.mem.zeroInit(S, .{});
}.s;
const cp: *S = &struct {
    var s = std.mem.zeroInit(S, .{});
}.s;

@AndrewCodeDev: what’s the rationale behind your makeUnique function???

1 Like

Ok perfect, we are now on the same page.

In Zig temporaries are immutable, this is unlike C (and all the imperative languages that I know of), so turning the temporary value into a var is not just “massaging” the code for the compiler but it is explicitly required in cases like this one.

As an example this doesn’t work either:

var allocator = std.heap.ArenaAllocator.init(gpa).allocator();
const.zig:24:54: error: expected type '*heap.arena_allocator.ArenaAllocator', found '*const heap.arena_allocator.ArenaAllocator'
    var allocator = std.heap.ArenaAllocator.init(gpa).allocator();
                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~
const.zig:24:54: note: cast discards const qualifier
/Users/kristoff/zig/0.13.0/files/lib/std/heap/arena_allocator.zig:26:28: note: parameter type declared here
    pub fn allocator(self: *ArenaAllocator) Allocator {

This is by design. In fact older versions of Zig did not do this and was explicitly introduced a few versions ago. I’m now looking through the langref because I was sure it was mentioned in there but I can’t find it so maybe I was wrong and it was never explained there.

In any case, now you know :^)

1 Like

Not a strong rationale either way - it just allows you to create unique structs by varying an argument. You can certainly go about it the way you have too - looks nice :slight_smile:

but now, i run into a problem i had in an earlier post…

const std = @import("std");
const print = std.log.info;

const S = struct {
    x: u32 = 10,
};

fn create(T: type, _: anytype) *T {
    return &struct {
        var o = std.mem.zeroInit(T, .{});
    }.o;
}

pub fn main() void {
    var vp = create(S, opaque {});
    const cp = create(S, opaque {});
    vp.x = 20;
    cp.x = 30;
    print("vp.x = {d}", .{vp.x});
    print("cp.x = {d}", .{cp.x});
}

the value vp.x is 30, not 20 as expected… the initial state of both structs are clearly identical; but somehow the compiler (thinking they’re constant) has elided the two… vp and cp are pointing to the same object…

said another way, how can i ensure the object returned by my create function is unique???

Yeah, good point - forgot about that little detail. You can do this and it works:

const std = @import("std");
const print = std.log.info;

const S = struct {
    x: u32 = 10,
};

fn create(T: type, comptime handle: anytype) *T {
    return &struct {
        var o = blk: {
            std.mem.doNotOptimizeAway(handle);
            break :blk std.mem.zeroInit(T, .{});
        };
    }.o;
}

pub fn main() void {
    var vp = create(S, opaque{});
    const cp = create(S, opaque{});
    vp.x = 20;
    cp.x = 30;
    print("vp.x = {d}", .{vp.x});
    print("cp.x = {d}", .{cp.x});
}

I forgot that if you don’t use the argument, it gets discarded and it can get folded into one instance. In this case, we are effectively throwing it away, but in a way that is opaque to the compiler.

Also, you asked if there is any motivation to this style of function. One affordance is that we can get the pointer through a pointer instance or we can re-reference the same thing using the same handle. This is well behaved:

pub fn main() void {
    var vp = create(S, 0);
    const cp = create(S, 1);
    vp.x = 20;
    cp.x = 30;
    print("vp.x = {d}", .{vp.x});
    print("cp.x = {d}", .{cp.x});
    print("(0).x = {d}", .{create(S, 0).x});
    print("(1).x = {d}", .{create(S, 1).x});
}

So here, I can get the same struct again using my key value 0 or 1 (or any other comptime known unique value). It’s an interesting detail that may be worth something (you could achieve something similar in other ways, too).

4 Likes