What is cast discards const qualifier error

I run into various cases where I encounter the error

cast discards const qualifier

In most cases I usually need to change from var to const or vice versa.

But I am not sure I actually understand what the error message really mean because most of the time I am not performing explicit cast, and the line of code the compiler points at in the error, is usually not one that has a cast also.

Would be nice to actually know what the compiler means when it errors with “cast discards const qualifier”.

1 Like

Hi, it sounds like you almost understand what’s going on, actually. There’s just an implicit cast or type coercion going on somewhere; this implicit cast might be fine and convenient for reading the const’s value, but not for mutating it.

I often see this when I write something like

pub fn changePath(path: []u8) !void {
    sys.debug.print(path, .{}); // ✅
    path[0] = '🫠'; // 😱
}

pub fn main() !void {
    try changePath("huh.txt");
}

The key to reading the debug message is to look at which types it says it found, and then look at the arrows in the excerpted code.

src/main.zig:10:20: error: expected type '[]u8', found '*const [7:0]u8'
    try changePath("huh.txt");
                   ^~~~~~~~~
src/main.zig:10:20: note: cast discards const qualifier
src/main.zig:5:25: note: parameter type declared here
pub fn changePath(path: []u8) !void {

If you imagine the arrows being a box where your bytes are, it’s telling you that only a certain type fits in that box, after all the explicit casting and coercion rules have been applied. You have to change the type it expects, or what you are giving it.

PS: to answer your question more directly, it’s telling you it tried to erase the const (coercion/discard qualifier) so you could pass that to the function’s signature, since the types are almost compatible for reading. But then something mutates it in that scope or even nested deeper, and the compiler has to abandon that assumption of const.

1 Like

The following is not an explanation, rather some little investigation.
Trying to compile this

const std = @import("std");
const S = packed struct {a: u8, b: u8, c: u8, d: u8};
pub fn main() void {
    const buf = [4]u8{1,2,3,4};
    const p: *S = @ptrCast(@alignCast(&buf));
    std.debug.print("s = {any}\n", .{ p.*});
}

throws

2.zig:9:19: error: @ptrCast discards const qualifier
    const p: *S = @ptrCast(@alignCast(&buf));
                  ^~~~~~~~~~~~~~~~~~~~~~~~~~
2.zig:9:19: note: use @constCast to discard const qualifier

Compare error and note!!!
Together they look a bit enigmatic, don’t they?

We can fix the program by two ways.

follow compiler advice, i.e

const p: *S = @constCast(@ptrCast(@alignCast(&buf)));

make buf mutable, i.e

var buf = [4]u8{1,2,3,4};

Now to original version.
As I understand in this case error is because buf is immutable,
but we cast it’s address to some pointer and we can potentially change
buf content via this pointer and

  • constCast() makes the pointer to be pointer to immutable data
  • making buf mutable… well, just makes it mutable, so the pointer is ok.

Things to remember:

  1. All function parameters in zig are constant.
  2. Function parameters can be pointers (memory locations)
  3. Function parameters can be value (contents of memory)
  4. You can have pointers to constant memory.
  5. The compiler will check to make sure you do not try to use constant memory where you should be using modifiable memory (illegal cast of const to var)
  6. The concept of constant memory enables a lot of optimizations (lets you re-use memory because you know you arent going to modify it!)
//! Run this file using: zig test path_to_this_file.zig
//!
//! We expect the first test to succeed and the second test to produce the const cast compile error.

const std = @import("std");
// All function parameters are constant in zig.

/// add one to the number I pass into this function by reference.
/// The reference (location in memory) is constant. But the contents of the memory
/// at that location can change.
pub fn addOneInPlace(num: *u8) void {
    num.* += 1;
}

/// The number I am passing into this function is constant and a new number is returned.
pub fn addOne(num: u8) u8 {
    return num + 1;
}

/// The location in memory is constant and the contents of the memory is constant.
pub fn addOneUsingReference(num: *const u8) u8 {
    return num.* + 1;
}

test "add one correctly" {
    var modifiable_number: u8 = 1;
    addOneInPlace(&modifiable_number);
    try std.testing.expect(modifiable_number == 2);

    const constant_number: u8 = 1;
    const new_constant_number: u8 = addOne(constant_number);
    try std.testing.expect(new_constant_number == 2);

    const new_constant_number_again = addOneUsingReference(&constant_number);
    try std.testing.expect(new_constant_number_again == 2);
}

test "add one illegally" {
    const constant_number: u8 = 1;
    addOneInPlace(&constant_number); // compile error here!
    try std.testing.expect(constant_number == 1);
}

jeff@jeff-debian:~/repos/zecm$ zig test experimental/test.zig 
experimental/test.zig:40:19: error: expected type '*u8', found '*const u8'
    addOneInPlace(&constant_number); // compile error here!
                  ^~~~~~~~~~~~~~~~
experimental/test.zig:40:19: note: cast discards const qualifier
experimental/test.zig:11:27: note: parameter type declared here
pub fn addOneInPlace(num: *u8) void {
                          ^~~

1 Like

I guess this is more or less about my example.

That’s undefined behavior (illegal behavior in Zig lingo). In this case, the compiler suggestion should not be followed. If the underlying buffer is const you are never allowed to change it, no matter how many casts and builtins you throw at it.
Behind the scenes, const data can go into the read-only section, which is protected from change by the OS, or it can be baked directly into the code, so it would have no address.
@constCast can ony be used if the pointed-to memory is mutable, but you ended up with a const pointer to it.

fn weirdMutationFunction(ptr: *const u8) void{
  const cast = @constCast(ptr);
  cast.* = 3;
}

fn main() void{
  var buffer: u8 = 0;
  const ptr: *const u8 = &buffer;
  weirdMutationFunction(ptr);
  std.debug.print("{}", .{ptr.*});
}

Output: 3

This works because buffer is actually mutable, despite what the pointer says.
Obviously, no one should write code like this. But sometimes you have a callback system or some kind of channel, and it expects a const pointer. So you can give it a const pointer and, at the other end, since you are sure you passed a pointer to mutable data, you can cast away the constness.

4 Likes

I got it.
Then it comes that my example can not be “fixed”, buf becomes mutable,
either explicitly (var buf) or by @constCasting.

No @constCasting has no effect on the actual mutability of a buffer.

It only allows you to get a mutable pointer and using that mutable pointer on a read only buffer to attempt to change something is illegal and can result in a panic / crash.

1 Like

Yes, that’s right, I put it wrong, sorry. I meant that with const buf and with @constCast() the program compiles (and runs) without errors.

  • const buf and no @constCast: error: @ptrCast discards const qualifier
  • var buf and no @constCast: ok
  • const buf and with @constCast: also ok

It can happen to work, but it still isn’t reliable because the data of the const buf can be placed into read only memory by the compiler and if that happens the program will crash. That is why you shouldn’t use @constCast to remove the const unless you actually know that the buf was declared with var.

@constCast should only be used when it is unavoidable, here it is clearly avoidable and when you know for certain that the buffer will always be mutable.

Using a const buf with @constCast will likely cause unexpected panics when the compiler changes and places more immutable things in read-only memory.
It also can cause logic bugs / undefined behavior because one part of your program claims the memory is immutable while another modifies it.

I guess maybe I misunderstood you a bit, if you meant to use a mutable pointer, without actually modifying anything, but if you aren’t modifying it, then it is better to just use a const pointer instead of casting away the constness and hoping nobody will actually use the mutable pointer for setting new values.

Passing around non const pointers to constant data is a bad practice and only makes sense in very rare corner cases, where you are sure that this particular instance won’t be modified while maybe other ones will be.
But people who work on such corner cases will be aware of those details.


Overall telling the compiler this data is constant/immutable and then changing it is a bad idea.

2 Likes

Ok, I am rephrasing my previous ramblings.
That program can not be “fixed” in a sense that:

  • I can use var buf, but what if I really want it to be immutable?
  • I can use @constCast(), but as you and @LucasSantos91 explained this is not a way to follow and… compiler is a bad adviser in this case.
1 Like

Then you use a constant buffer and a pointer to constant data *const S:

const std = @import("std");
const S = packed struct { a: u8, b: u8, c: u8, d: u8 };
pub fn main() void {
    const buf = [4]u8{ 1, 2, 3, 4 };
    const p: *const S = @ptrCast(@alignCast(&buf));
    std.debug.print("s = {any}\n", .{p.*});
}

Do you mean something different?

phew!
here is absolutely legal variant:

const std = @import("std");
const S = packed struct {a: u8, b: u8, c: u8, d: u8};
pub fn main() void {
    const buf = [4]u8{1,2,3,4};
    const p: *const S = @ptrCast(@alignCast(&buf));
    std.debug.print("s = {any}\n", .{ p.*});
}

No, this is exactly what was needed, immutable pointer to immutable data.
It was that note: use @constCast to discard const qualifier that disoriented me.

1 Like

maybe there should be two notes where the first is:
note: use a constant slice/pointer or
note: use @constCast to discard const qualifier

Might make sense to think about the different cases and how the error message could be improved in the compiler, I don’t think the current note is that good, it is quite rare where discarding const is actually the right thing to do.

2 Likes

Could you please share some of these cases?

Just to be super clear: it’s a “note”, not a “hint”. So it’s not really a suggestion or advice, it’s just informing the programmer the existence of a relevant language feature.

I’ve been very strict rejecting contributions that add “hints” to the compiler, because in my experience such hints can never be 100% accurate (otherwise they wouldn’t need to be diagnostics, the language would just allow the construct), and so hints are not allowed in compiler diagnostics. They must inform rather than prescribe. Similarly, I think this is what good documentation comments do.

4 Likes

Yes, this may be somewhat confusing.
But now I understand what compiler complains about for my example with pointer casting at least. Sorry for repetition :slight_smile: Here is context:

const buf = [4]u8{1,2,3,4};
const p: *S = @ptrCast(@alignCast(&buf));

and here is compiler message:

error: @ptrCast discards const qualifier
const p: *S = @ptrCast(@alignCast(&buf));
              ^~~~~~~~~~~~~~~~~~~~~~~~~~

Compiler kinda says:

"Hey, man, listen here:

  • on the RHS of the assignment you have &buf, a pointer to immutable data, since you declare buf as const buf in the line above, don’t you?
  • on the LHS of the assignment you have *S, a pointer to mutable data

You are likely going to do something wrong.
"