Is it possible to apply const on return result adaptively?

For example, is it possible to write Zig code like

const T = struct {
	x: u32,
};

const S = struct {
	t: T,
	
	pub fn getT(s: *constness S) *constness T {
		return &s.t;
	}
};

If by adaptively you mean being able to choose between getting a mutable vs immutable pointer to T, than you could define two respective methods and then statically dispatch on them whenever needed.

const T = struct {
    x: u32,
};

const S = struct {
    t: T,

    pub fn getT(s: *S) *T {
        return &s.t;
    }

    pub fn getConstT(s: *const S) *const T {
        return &s.t;
    }
};
2 Likes

You could just get t directly instead of using a getter function. Then it has the desired behavior:

const varS: *S = undefined;
const constS: *const S = undefined;
const varT = &varS.t;
std.debug.assert(@TypeOf(varT) == *T);
const constT = &constS.t;
std.debug.assert(@TypeOf(constT) == *const T);
2 Likes

For simple use cases, it is okay to get it directly. Sometimes, the logic is more complex so it is best to wrap the logic in methods.

1 Like

This is similar to what I did now.

const S = struct {
    t: T,

    pub fn getT(s: *S) *T {
    	// ... more logic here
        return &s.t;
    }

    pub fn getConstT(s: *const S) *const T {
        return @constCast(s).getT();
    }
};

I just don’t like to use the @constCast function (it is helpful for avoiding some code duplication).

1 Like

Why not put the logic into the const function then?

    pub fn getConstT(s: *const S) *const T {
        // ... more logic here
        return &s.t;
    }

    pub fn getT(s: *S) *T {
    	_ = s.getConstT();
        return &s.t;
    }
3 Likes

My example is over-simplified. The @constCast function will be used anyway in reality use cases:

    pub fn getT(s: *S) *T {
        return @constCast(s.getConstT());
    }

You can definitely propogate constness from one item to another based on a single function call by switching out the return type at comptime.

const MyType = struct {
    number: usize = 42,  
};

pub fn getFieldPtr(
    x: anytype, 
    comptime field: []const u8
) @TypeOf(&@field(x.*, field)) {
    return &@field(x.*, field);
}

pub fn main() !void {

    const x: MyType = .{ };
    var y: MyType = .{ };

    const a = getFieldPtr(&x, "number");
    const b = getFieldPtr(&y, "number");

    const a_name = @typeName(@TypeOf(a));
    const b_name = @typeName(@TypeOf(b));

    std.debug.print(
        \\
        \\ Type A: {s}
        \\ Type B: {s}
        \\
        , .{
            a_name, 
            b_name
    });
}

Prints:

 Type A: *const usize
 Type B: *usize

So yes, it certainly is possible to adapt return types based on the characteristics of a parent type (or any comptime data, for that matter).

1 Like

Edited - see post 9 for clarification: Is it possible to apply const on return result adaptively? - #11 by AndrewCodeDev

Using @This() directly leads to conflicts regarding the member function deduction, but this can be bypassed by using anytype. The rest of the critique and observations are valid, but it is not impossible to achieve what OP was looking for.


The problem you’re running into is the specification of the @This() argument because it doesn’t split hairs - it’s either const or not becuase @This() is not generic or context sensitive.

If you write your code in a C-style way, you can make all your getters/setters and other “member” functions work adaptively.

const MyType = struct {
    number: usize = 42,
};

pub fn getNumberPtr(x: anytype) @TypeOf(&x.number) {
    // assert that x is a pointer and the child type is MyType
    return &x.number;
}

// other more involved funtions...

The problem here is that you’re having to deduce x because the constness of a pointer is actually a different type… @TypeOf(a) == @TypeOf(b) -> false.

Since the simple cases are already handled natively by doing something like:

const x = &y.number; // x is const/mutable based on y's qualifier

Then what you’re really left with are the complex cases. But as was demonstrated above, you can easily make those free functions that expect a specific Child-Type and make them return whatever pointer type you want if you get creative about the interface.

What you’re doing here is very similar to a pattern proposed by Scott Meyers for member function deduplication:

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

In this case, it makes sense because you have two member functions named the same way (they’re just overloads) and we’re deduplicating the core logic. In their case, they can only ever call get and whatever type they are qualified determines which one gets called.

In your case, I can be a non-const S and call s.getConstT and s.getT but as soon as I’m a const S, I can now only call s.getConstT and s.getT is a compile error. So there’s an asymetric interface here where as in the C++ case, it’s perfectly symmetric… it’s just get in both cases.

1 Like

The solution is a little verbose, and the function can’t be called as method.
But I think it solves the need in my first comment.
Zig meta programming is cool.

1 Like

@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 :slight_smile:

3 Likes