Mutation via `const` pointers

In Zig, a pointer type having a const qualifier means that you cannot write to the memory which
the address points to using that pointer:

const Foo = struct {
    bar: i32,
};

pub fn main() void {
    var x = Foo{ .bar = 5 };
    const p: *const Foo = &x;
    p.bar += 1; // compile error: 'cannot assign to a constant'
}

So a *const T pointer can be thought of as an immutable reference to T, the meaning which the programmer takes from the type is “I’m just looking at T, not changing it!”. This is useful because it helps the programmer when reading code to know what state can and can’t change.

Now what happens if the memory which the const pointer addresses contains another pointer, which is not const?

const Foo = struct {
    bar: *Bar,
};

const Bar = struct {
    baz: i32,
};

pub fn main() void {
    var bar = Bar{ .baz = 5 };
    var foo = Foo{ .bar = &bar };

    const p: *const Foo = &foo;
    p.bar.baz += 1;
}

This code compiles and runs with no errors!

This arguably contradicts the implied meaning of a *const T: “I’m just observing T, I won’t touch it!” Of course technically that is the case - but it is changing something else via the constant pointer through a non-const pointer at that location. This is something the programmer might not expect if they have internalized the aforementioned implication of a const pointer, which (again) is a useful one.

This struct me as something that Rust would be terribly concerned about, so I tried it:

struct Foo<'a> {
    bar: &'a mut Bar,
}

struct Bar {
    baz: i32,
}

pub fn main() {
    let mut bar = Bar { baz: 5 };
    let foo = Foo { bar: &mut bar };
    let p = &foo;
    p.bar.baz += 1;
}

Surprisingly, rust doesn’t have a problem with this code!

Just kidding.

error[E0594]: cannot assign to `p.bar.baz`, which is behind a `&` reference
  --> const.rs:13:5
   |
13 |     p.bar.baz += 1;
   |     ^^^^^^^^^^^^^^ `p` is a `&` reference, so the data it refers to cannot be written
   |
help: consider changing this to be a mutable reference
   |
12 |     let p = &mut foo;
   |              +++

I’m not sure how exactly rust enforces this. A simple idea would be to demote all the pointer types of a type behind a const pointer to also be const, but I don’t know how complicated that would be to add to the Zig compiler. Still, I think this is a good semantic for a low-level language to have.

i dont think it would be hard to implement, but should it be.

doing so would be changing the semantics of the language, in a subtle but impactful way.

1 Like

Are you talking about implementing this constraint in the compiler or as a user? In either case I’m not sure what this means.

I read this as “but it should be”, my bad.

Well that’s really why I created the topic. I understand that this is a breaking change to language semantics, but Zig is still able to make breaking changes, so why not discuss ideas like this?

As someone coming from high-level dynamic languages such as Python/Bash/JS, I was expecting that these kinds of things would bother me as well.

But I have to say that the way Zig behaves now seems natural to me. A struct “doxxed” address, but the address was not prohibited.

I think I see why you might want that implication to be true, but the question would be how effective would it really be. Even in your example you could actually have changed foo; both structs are vars already.

Sure, it’s just an example, but even the way I write code nowadays (just for learning), I usually end up creating a writable buffer []u8 somewhere in main(), wrapping it in a FixedBufferAllocator and passing the allocator around. I could still just write to the buffer from main() regardless of the pointer/type semantics.

At some point, it’s a process and it has to own the memory.

Yes, that’s true. I like “pointer doxxing” as a term for this.

The main use for this would be communicating intent through type signatures, and having the compiler guarantee that intent is true. This is already what const pointers are for, I’m just talking about making that restriction more strict in order to align with the implication which const pointers already communicate.

I can’t think of many cases where I would want this over the current behavior. It’s pretty common to want a const struct that contains pointers to mutable data, how would that be accomplished if the default was this transitive const semantics?

5 Likes

That’s a good point. One way would be to have this be a separate qualifier which *const could coerce to, but I don’t think the usefulness of the concept warrants that.

Maybe I just need to retrain myself to think about const pointers differently, I think my perception probably carried over from learning rust.

2 Likes

If you don’t want the baz field to be changeable you already have the option to declare bar as *const Bar too, or even change it to just Bar.

I don’t quite get what the point is with declaring that it is mutable and then expecting/wanting otherwise. I think I would rather have a simple type system, instead of one that asks me to trace my steps about how I got access to some value to see whether I should be allowed to modify it.

I think having the ability to either modify it or not based on simple direct types (you can see in the code) is more preferable to me, then having to reason about chains of access patterns.

If it were implemented, I would also wonder whether it even makes any sense without also adding private fields to the language, which I don’t miss at all, if anything I am quite happy being able to access fields and look at them, or even change them if I want to.

It seems to me like it would turn the language into something I would enjoy less.
I don’t really want to write some kind of algebra of types, where I need to think about encapsulation, security, soundness, complex type thingies, argue with the type system, all this kind of stuff seems like a distraction from actually writing a program, with Zig I can just write my program, without the type system becoming an annoyance that gets in my way and starts preaching at me, about what it thinks I should do to construct my program. I would rather have the option to shoot myself in the foot, then live with the shackles of that security and the drudgery it brings.

I can understand wanting these sorts of high level type features in something like haskell or rust, those languages are made for typestronauts, I personally don’t think these fit Zigs vibe and what it is about.

But everyone has a different idea of what that might be and it can be difficult to communicate or explain these nuanced ideas, about what somebody thinks makes sense or why it wouldn’t be good. I also can’t fully articulate my thoughts, because partly I just have a gut feel of “ohh noo, that seems like something I wouldn’t enjoy”.

But a part explanation might be this:
I am tired of frameworks / languages / ecosystems that make me jump through annoying or unnecessary things, I want a language that works for me, instead of making me do a whole lot of work to satisfy the language.

I guess the part that confuses me is how / where / when would this help users of the language?
From my perspective I would expect this to hinder me instead of help me in 99% of cases.

sneaky pun, at first my brain didn’t even register it and just read it as struck

7 Likes

Because Foo might need a mutable pointer, since some of it’s methods do modify bar, but it would also be nice to specify when a method doesn’t do anything to foo, including bar.

That’s true, I agree that just being able to do what you want with your data is a big part of Zig. That said, having const pointers in the first place shows that Zig cares at least on some level about enforcing immutability, but that concept is kind of incomplete in the sense that a const pointer can still be used to cause mutation.

I definitely agree with most of your points, but I still think it could be easy for people who are new to Zig to see const pointers and assume the “no mutation” promise, when it’s only mostly true.

What?! That was a complete accident, don’t know how I didn’t see that :sweat_smile:.

1 Like

To me it makes more sense that const doesn’t imply recursive const-ness per se, although I’m sure you could make a comptime function to do that if you want. Const is just for the one thing it points to.

2 Likes

Reading your diagram then:

const foo: *const Foo means that foo.bar is a const *. That then makes this make sense. The address is immutable, but the data is not.

1 Like