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.
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.
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.