Using @constCast

What is Mutability?

The mutability of a variable determines if the variable is subject to change after being initialized.

// x is immutable - cannot change
const x: i32 = 42;

// x is mutable - can change
var x: i32 = 42;

Typically, pointers do not allow you to remove the const qualifer on a variable. For more information on this, see: Mutable and Constant Pointer Semantics

This issue seems settled until we involve @constCast.

What is @constCast?

The @constCast builtin function drops the const qualifer on what a pointer is pointing to. More formally, @constCast is a function that can perform the following conversion:

@constCast: *const T -> *T

It is also important to note that @constCast can work with optional pointers as well:

@constCast: ?*const T -> ?*T

In otherwords, we get back a pointer that can change the underlying value that it is pointing to.

Should const qualification ever change?

Const qualification actually changes quite a bit depending on the circumstances. Let’s say I have a struct called Foo. Foo has a member function bar that takes a const pointer to itself and another member function baz that takes a non-const pointer to itself:

const Foo = struct {
    pub fn bar(self: *const Foo) void {
        // do something here...
    }
    pub fn baz(self: *Foo) void {
        // do something here...
    }
};

// later...

// make a mutable Foo
var foo: Foo = .{};

// uses const qualifier
foo.bar();

// doesn't use const qualifier
foo.baz();

In this circumstance, our variable foo had two pointers to it - one with const qualification and one without. One member function can change the underlying value of foo, and the other cannot. The const qualification of foo depends on the circumstance.

@constCast in Use

Here are two examples where @constCast can be used and why they are valid.

  1. std.mem.Allocator's destroy function

In the source for code for the Allocator Interface, you’ll see the following code for destroy:

/// `ptr` should be the return value of `create`, or otherwise
/// have the same address and alignment property.
pub fn destroy(self: Allocator, ptr: anytype) void {
    const info = @typeInfo(@TypeOf(ptr)).Pointer;
    if (info.size != .One) @compileError("ptr must be a single item pointer");
    const T = info.child;
    if (@sizeOf(T) == 0) return;
    const non_const_ptr = @as([*]u8, @ptrCast(@constCast(ptr)));
    self.rawFree(non_const_ptr[0..@sizeOf(T)], log2a(info.alignment), @returnAddress());
}

The variable non_const_ptr is being assigned from a call to @constCast. Why does this work at the level of the type system? To understand this, we should look at the function create:

/// Returns a pointer to undefined memory.
/// Call `destroy` with the result to free the memory.
pub fn create(self: Allocator, comptime T: type) Error!*T {
    if (@sizeOf(T) == 0) return @as(*T, @ptrFromInt(math.maxInt(usize)));
    const ptr: *T = @ptrCast(try self.allocBytesWithAlignment(@alignOf(T), @sizeOf(T), @returnAddress()));
    return ptr;
}

The create function returns an Error!*T, but the relevant portion of the return type is the *T. We can see that T in this circumstance begins its life as non-const. Much like our example with Foo above, T may get an additional const qualifier appended to it at some point, but it is not instantiated as const to start. Another way to say this is that we are returning T to its initial mutability in the destroy function.

  1. Member function deduplication

This pattern was originally popularized via Scott Meyers. The idea is to write a constant version of a member function that can be called from a non-constant version of the same type. Here is an example:

// helper to deduce if we are pointing at a const-thing
fn isConstPtr(comptime T: type) bool {
    return switch (@typeInfo(T)) {
        .Pointer => |p| p.is_const, else => false
    };
}
// helper to promote a non-const pointer to a const pointer
fn asConst(comptime T: type, value: *const T) @TypeOf(value) {
    return value;
}
// helper to figure out if we should return a const or non-const pointer
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());
    }
}  

In this example, when we call the function last, it will change behavior based on whether or not the self variable is const qualified. To understand how self: anytype works, please see: Generic Programming and anytype

In the circumstance that we are const qualified, the first switch branch will fire and return a pointer to our last element that is also const qualified. This is often referred to as propagating const-ness. If we’re const qualified, our return value will be const qualified too.

In the circumstance that we are not const-qualified, we add a const qualifier using asConst, recall the same function to invoke the const qualified branch, and then cast away its const qualification upon return. Thus if we are not const qualified, we return a pointer that is not const qualified too.

When is @constCast a bad idea?

Removing const qualification can be a bad idea if you are working from variables that begin their life as const. The compiler sees that the variable is const and makes many assumptions about the stability of the variable’s value. Why wouldn’t it? We’ve said that the object is fundamentally immutable. This allows the compiler to perform many valuable optimizations.

Casting away the const qualifier on a constant variable can violate these assumptions and cause strange behavior.

What is the general rule for using @constCast?

The pattern that works is: non-const x -> *const x -> @constCast(ptr)

Do not go in this direction: const x -> *const x -> @constCast(ptr)

More explicitly, do not cast away the const qualifier for pointers that reference something that is initialized as const. The pointer and the compiler will have a different interpretation of what the guarantees are surrounding said data.

4 Likes

This Doc is very general. Some of the wording could be more specific and we could also handle casting away const qualification inside of functions that expect a variable to be constant. If you see any discrepancies or have more examples, please add your revisions.

I know I could edit this myself, but it wasn’t 100% clear to me what you were trying to say here. In any case, I would suggest to use x for the const and y for the variable, and maybe edit this paragraph, so that it becomes less ambiguous:

It is important to understand that x is an alias for some data on your computer. In other words, x is not the data itself but a way to refer to some data. In this case, the data that x aliases cannot be changed by using the alias x.

Ah, so the point I was making is that when we make a variable named x, it’s easy to confuse the variable for it’s own underlying data. The name x is just a handle to that data - that’s why we can take pointers to that data and modify it without using x. So x may be const qualified, but something else that views the same data that x aliases may not be.

Likewise, with const qualified literals, the compiler can replace those instances of the variable with the actual value that the variable is aliasing. That’s part of why @constCast can create weird behavior - the variable has been expunged from the code and replaced with direct values.

I honestly debated on including that part. It may not be particularly helpful in this case.

1 Like

@gonzo I’m going to remove that part becuase the article reads just fine without it.

1 Like