Method overloading and deduplication using @constCast

In the following topic opened by @zigo, we discussed using @constCast patterns to deduplicate code in the style of Scott Meyers: Is it possible to apply const on return result adaptively? - #11 by AndrewCodeDev

It turns out, you can do this in Zig, but I’m trying to think of some more practical examples of what this could be used for (if anything). Here’s the simple version of the technique outlined in the original topic (please read it for for information):

fn isConstPtr(comptime T: type) bool {
    return switch (@typeInfo(T)) {
        .Pointer => |p| p.is_const, else => false
    };
}

fn asConst(comptime T: type, value: *const T) @TypeOf(value) {
    return value;
}

const Foo = struct {
    value: usize = 0,
    pub fn getPtr(self: anytype) @TypeOf(&self.value) {
        if (comptime isConstPtr(@TypeOf(self))) {
            // do some things you don't want to duplicate
            return &self.value;
        } else {
            return @constCast(asConst(@This(), self).getPtr());
        }
    }
};

pub fn main() !void {
    var bar = Foo{ .value = 42  };  
    const baz = Foo{ .value = 42  };  

    const bar_ptr = bar.getPtr();
    const baz_ptr = baz.getPtr();

    std.debug.print("Ptr Types: ({s}, {s})\n", .{
         @typeName(@TypeOf(bar_ptr)),
         @typeName(@TypeOf(baz_ptr)),
    });
}

And this prints as expected:

Ptr Types: (*usize, *const usize)

I’ve been aware of the Meyers constCast pattern for years now, but I haven’t made much use of it, but this basically allows writing what is equivalent to an overloaded member function. I’d just like to see more examples and hear thoughts about where this may be useful?

Here’s a link to some more info on the topic: How to Share Code with Const and Non-Const Functions in C++ - C++ Stories

And here’s a blurb from that link:


Let’s reach out to Scott Meyers and in his Effective C++ 3rd Edition. On page 23, Item 3 (on using const) we can read that a non const function can safely call const one. To achieve this, we can leverage <const_cast>…


Does this assumption hold true in Zig? Anyhow, I’m curious to hear other people’s thoughts on this.

I mean, BoundedArray does do this.

Yes - I brought up that example in the original post.

The BoundedArray example is an interesting case because the return type is mirrored based on the self type of it’s slice function. In practice that looks like…

f(*T) -> []U and f(*const T) -> []const U

However, bounded array has a subtle but important difference because the return statement automatically binds the const qualifier due to the return type. It isn’t an explicit @constCast. It also doesn’t internally dispatch recursively and strip away a const qualifier.

I’m willing to believe in the safety of this method because you can call const methods functions in non-const methods (it just appends the qualifier to the pointer).

So one case this could be handy is in the case of dependent getters… say I have a getter that returns the last item appended to a list… we’ll call it last and we can write a helper function here to cleanup returns.

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());
    }
}  

As the implementation of the function itself gets more complex, this option because more appealing (say for instance if we needed to look up a node in a tree), then this becomes particularly useful.

However, it can be easily abused too. First, we should never go in the opposite direction - don’t ever cast down from const to call a non-const function. That could cause the optimizer to botch things quite badly - cast up and then back down.

Second, I’d say it’s best to not append additional code to the casting branch - that actually creates separate functions.

As far as I can see up to this point, the most appropriate use case for this is to swap out constness on returned pointers or pointer-like types (like slices). Thanks to that, we can overload our methods.

2 Likes