What is const-ness exactly?

I’m trying to distinguish what is const-ness and how it is related to: values (memory), pointers, variables, and types. It feels like these things have their own life.

I’ve already looked at:

And it feels like I’m just getting more confused :smiling_face_with_tear:

For example:

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

test "const-ness" {
    const foo: u8 = 1;
    var bar: u8 = 1;
    assert(@TypeOf(foo) == @TypeOf(bar)); // u8 == u8

    const foo1 = &[_]u8{42};
    var bar1 = &[_]u8{42};
    assert(@TypeOf(foo1) == @TypeOf(bar1)); // *const [1]u8 == *const [1]u8
}

$ zig test file.zig 
All 1 tests passed.

Shouldn’t the test have failed? I expected foo to show up its const-ness somewhere (ie. in its type). Also, I didn’t understand why &[_]u8{...} translates into a const pointer, ie. *const [1]u8. I’d greatly appreciate any clarification, or perhaps information on how I can inspect const-ness during compile or runtime.

As for now, my understanding boils down to the following:

  • In types, you can’t const values (eg. structs), only pointers. So you can’t write: const foo: const u8 = 42;
  • The const-ness of values can be expressed only through the const qualifier of a variable, eg: const name = ...
  • “Const pointer” means a pointer to immutable space of memory.
  • Pointer itself does not carry any const-ness in its type.
  • Instead, the const-ness of a pointer can only be expressed by applying the ‘const’ qualifier to a variable that contains it.
1 Like

first of all, good job on trying to map your knowledge, that’s a good way of making progress. these points that I’m quoting are not correct and as you improve your understanding you will be able to amend them and reach a sound conclusion

a const pointer is a pointer that doesn’t allow you to write the memory it points to. you can create const pointers for both const data and mutable data, but you can’t create a non-const pointer to const data

var a: usize = 1;
const b: usize = 2;

const a_const_ptr: *const usize = &a;
const b_const_ptr: *const usize = &b;

const a_ptr: *usize = &a;
const b_ptr: *usize = &b; // won't compile because `b` is declared const, so nobody can mutate it

a_ptr.* = 5; // ok
a_const_ptr.* = 5; // won't compile because a const pointer (ie a pointer of type `*const T`) doesn't let you write to the memory it points to (even if the original declaration was `var`)

var another_ptr: *const usize = &a;
another_ptr = &b; // the variable that holds the pointer can be mutated (because of `var`) so we can put in there a different pointer, but no matter which pointer value we set it to, we won't be able to write through it because the type of the pointer is `*const usize`

var more_ptr: *usize = &a;
more_ptr = &b; // won't compile: since `b` is a const variable, we can't create pointers to it that allow you to mutate it. 

Hopefully this should help.

5 Likes

To support what @kristoff is saying here, the issue of “pointer-to-const” and “const-pointer” is an age-old issue that I see a lot of mistakes with even in professional code (perhaps even especially there lol).

I find it very helpful to pause for a moment and remember that, fundamentally, a pointer is a number. It’s a memory address - with modern languages, we also put on a type to express what lives at that memory address. This also brings in information about things like alignment but at the end of the day, we’re talking about a number of some sort.

A const-pointer basically means that I cannot change what address the pointer is currently set to (I can’t change its numeric value).

A pointer-to-const means that I cannot change what lives at the memory address. I can, however, change the pointer itself because it’s just a number and it’s not const.

Keep up the good work - it’s always good to see people digging into fundamentals :slight_smile:

4 Likes

Personally that’s what I call a “const pointer”. Pointer-to-const seems to say that the memory being pointed to by the pointer is immutable (const), but that’s not necessarily the case as you can create a *const T pointer for a mutable variable.

var a: usize = 1;
var ptr: *const usize = &a; // `a` is mutable, but this pointer is "read-only" as far as the type system is concerned

So constness as part of a pointer type definition is a property of the pointer, not the memory it refers to, IMO. Of course memory can be writable or not and pointers need to reflect this fact, but it’s not a 1:1 mapping.

Not an expert though, this is just how things are organized in my mind.

2 Likes

Sure - I can agree with that sentiment.

If you’re trying to understand the system as a whole, then immutability is a much trickier subject. For instance, one declaration may say const but then at another moment in the system, const can be casted away. If we think of a pointer as a viewport, then it has a perspective of what it’s viewing.

I’m mostly focusing on the semantics of a single pointer itself. For instance, if I wanted to make a slice that’s just viewing objects but isn’t intended to modify them, then as far as that slice is concerned it’s looking at const memory (through this viewport, we see const).

As a matter of fact, I think this brings up an interesting question about types in general and I’ll propose it as a thought experiment…


If you have to make a judgement about mutability at a system level and all you have is a single pointer, what can you reasonably say about that data in the system as a whole?

In the case where you have a pointer-to-const (*const T), I can’t assume much. This may only be a way to view data that in some other place is mutable. This is because non-const can be promoted to const.

In the case of pointer-to-non-const (*T), I can reasonably assume that the data I am looking at is mutable throughout the system. This is because (in general), const is not easily demoted to non-const.


So I think we’ll disagree on the verbiage here. To me, const-pointer says that the pointer itself is const in the same way that const-integer says the integer itself is const. I think of pointers as values that can be changed, so mentally I apply the const-ness to the pointer itself in that case.

3 Likes

I initially found a little jarring in my mind that you can do this:

var a: u8 = 42;
const ptr: *u8 = &a;
ptr.* = 13;

Specifically the ptr.* = 13 seemed odd given I had declared ptr as const. But after analyzing it more deeply, I realize that the ptr.* is not a modification of the pointer, it’s the act of de-referencing and obtaining whatever it points to, and that’s what’s being mutated, not the pointer itself. I guess it’s the fact that I’m used to languages where the . operator is always used to access fields and thus if you assign to the result of that dot operation, you are mutating the structure the field belongs to. So in my case, it’s all a matter of re-wiring my brain to become accustomed to Zig’s syntax.

1 Like

There are two things at play here, there’s understanding the concept itself, and then there’s the naming convention. The naming convention is just a convention. If someone says “const pointer”, do they mean the variable is const or the type is const? In the Zig community it’s the latter, per Loris.

But in code, the difference between a variable and a type is pretty clear. A variable is its own thing, it is its own memory, and when you specify “const” that means the variable itself cannot be modified. If my variable holds a pointer, or an index into an array, or a string that maps to a particular slot in a hash table, and the variable itself is const, that means I’m not changing the data in the variable itself. However, that has nothing to do with the data being pointed to, or referred to, or that we would go look up based on our variable. It is the type that tells us what we’re pointing to, and the “const” in the type only determines whether we’re allowed to modify it from this variable (but just because we aren’t allowed to modify it, doesn’t mean that no one is!).

To answer your specific question about why you get a const from taking the address of an anonymous array literal: it has nothing to do with the variable’s “const” that there is a “const” pointer in the type. Again, these are two completely different kinds of “const”, and actually they can be mixed in any combination. You can have a “const” variable with a “const” pointer inside it, or a “const” variable with a non-“const” pointer inside it, or a non-“const” variable with a “const” pointer inside it, or a non-“const” variable with a non-“const” pointer inside it.

When you take the address-of like you did, Zig simply has to give you the default pointer type, and it’s up to the language designer to choose whether it’s best to default to a const pointer or a non-const pointer. To some extent, you could say the choice is arbitrary, but actually Zig has some really good reasons for defaulting to giving you a stricter type by default.

For one, defaulting to stricter invariants and more “const”-ness is generally considered a good idea for a good reason. It can force you to be clear about what actually can and cannot be modified and who is allowed to do it. Zig favors being clear for the reader.

This was actually changed for 0.10, motivated by specific bugs. At the moment I can’t find the particular issue but it was a problem with the common way of declaring an allocator and it was error-prone in some way. I don’t quite remember now but you can read the announcements without the rationale:

https://ziglang.org/download/0.10.0/release-notes.html#Address-of-Temporaries-Now-Produces-Const-Pointers

3 Likes

First of all, thanks everyone for great answers!

I think @kristoff nailed the whole thing with pointer const-ness well by saying “a const pointer is a pointer that doesn’t allow you to write the memory it points to.” I’d go with this definition by now.

Even though, in the same spirit as @AndrewCodeDev, I view pointers as numbers that point somewhere in the memory (hopefully :)), defining a “const-pointer” as “an address that you can’t change”, would be misleading. If we look at std.builtin.Type.Pointer, we can clearly see that the pointer in the Zig type system is represented as a struct having is_const field, which probably states “pointer cannot be dereferenced to change the value it points to”. I think that is in sync with the Loris’ definition.

1 Like

I think this trips everybody when they first come to a language like Zig from a higher level language.

In my opinion this starts from the fact that it might not be clear that const to the left of a variable name, and const inside a pointer type are two different concepts.

If Zig wanted to make this less confusing for newcomers it could rename one of the two keywords

var a: usize = 1;
const ptr: *read-only u8 = &a;

read-only is a bit of a mouthful so I wouldn’t recommend that keyword specifically, I just wanted to show an example.

That said, pointers in Zig relate also to pointers in C and if you use a different keyword than what C uses, then you have to explain people how the two things map.

On the topic of nomenclature, I use “const pointer” to talk constness of the pointer type because that’s usually what you end up talking about the most (and “const pointer” is a relatively short expression). Most of the time, when you try to change the pointer held in a variable, and you get an error because it was declared const, that’s not a big source of confusion.

Glad it worked out for you :^)

I’m originally a C++ guy, so some of my language habits are inherited (lol).

I’m going to mark @kristoff’s answer as the solution to this thread. Great work, everyone :slight_smile:

1 Like

Thanks Andrew! I appreciate your foundational and, sometimes, philosophical approach on the matter :slight_smile:

1 Like