@zigo, I decided to dig into this more to see what options are actually available on member functions. Turns out, it is possible, it’s just extremely under documented.
Here’s the key to the whole thing:
pub fn foo(self: anytype) ...
Self can be anytype
. That means you can actually deduce self inside the call itself. Here’s an example:
const std = @import("std");
const Foo = struct {
value: usize = 0,
pub fn getPtr(self: anytype) @TypeOf(&self.value) {
std.debug.print (// check that we're getting a pointer
"Self Type: {s}\n", .{ @typeName(@TypeOf(self)) }
);
return &self.value;
}
};
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)),
});
}
Here’s what that prints:
Self Type: *main.Foo
Self Type: *const main.Foo
Ptr Types: (*usize, *const usize)
We can see that bar
is non-const because it’s declared with var
while baz
is the opposite. Even in an example like the following:
pub fn getPtr(self: anytype) *const usize {
// check that we're getting a pointer
std.debug.print(
"Self Type: {s}\n", .{ @typeName(@TypeOf(self)) }
);
// return a value that has nothing to do with self
const value: usize = 10;
return &value;
}
The self
type is still being deduced as a pointer. This is extremely fortunate because that means that the anytype
parameter for self prefers pointers first and we don’t have to worry about if self
was copied by value and end up returning invalid memory on the stack (I also tested on ReleaseFast
… same thing).
So this is very peculiar but it makes the pattern you wanted to do completely possible. Let’s write an isConstPtr
and asConst
helper functions and use that to internally dispatch (note: you could use the @as
builtin instead, but I find it to be uglier)…
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());
}
}
};
For casting arguments, here’s an example from bounded_array.zig: zig/lib/std/bounded_array.zig at master · ziglang/zig · GitHub
/// View the internal array as a slice whose size was previously set.
pub fn slice(self: anytype) switch (@TypeOf(&self.buffer)) {
*align(alignment) [buffer_capacity]T => []align(alignment) T,
*align(alignment) const [buffer_capacity]T => []align(alignment) const T,
else => unreachable,
} {
return self.buffer[0..self.len];
}
You can see here that they are determining the type with a switch statement and deciding which type of slice to return so you can get fancier in the return types than my simple example (especially if casting is involved).
So yes, you could do some serious deduplication with this pattern. I’m surprised this isn’t more well known but I guess that’s why we’re here to have this dicussion, lol.
Anyhow, I’m editing my other comment because I was incorrect about the status of member functions as they relate to this technique. I’m going to open up a brainstorming topic on this because I think we have some more experimenting to do