Const pointer semantics

Consider the following function that I want to be able to use for both *const Foo and *Foo arguments:

fn getBar(foo: *const Foo) *Bar {
    //return foo.bar;  //errors out
    return @constCast(foo.bar);
}

I’m here to argue that the language semantics should be changed so that the @constCast here is not needed, and instead any potential const mismatch be identified at the call sites instead:

const foo_v: *Foo = ...;
const foo_c: *const Foo = ...;

const bar_vv: *Bar = getBar(foo_v); //pass
const bar_vc: *Bar = getBar(foo_c); //fail
const bar_cc: *const Bar = getBar(foo_c); //pass
const bar_cv: *const Bar = getBar(foo_v); //pass

This would allow the same function to be used for both const and non-const pointers without compromising const-safety.

const in this case is a little bit unintuitive, because it essentially widens the set of possible input of a function, while it does the opposite to the output.

Do you have any real usecases where this would help? In my experience I take a *T if I change anything in T (thus requiring a mutable pointer) and a *const T otherwise (allowing both cases), which hasnt let me down yet.

In this vein I do see some use for propagating sentinels, which Ive ran into using std.mem.cut* with a [:0]const u8.

In general this is already possible with fn (v: anytype) @TypeOf(v) { ... }, but its clunky

As for the use case I made the post in the first place because I encountered this in my code:

    fn getEntity(g: *const Game, id: EntityID) *Entity {
        return @constCast(&g.entities[@intFromEnum(id)]);
    }

I’m using this function both in the game logic which takes *Game (and assigns it to *Entity), and in the renderer which takes *const Game (and must assign it to *const Entity).

The solution for this Ive seen used here and there is to have fn get() as well as fn getConst() which is mostly fine if the code is a trivial typesafe getter like this imo. Not a fan of the duplication but there isnt a good fix.

To go deeper into your proposal, this would bring the type forwarding system (I think its called RLS) to funtion return types. The immediate problem I see is how you keep sane function signatures, something like fn fooGetter(*maybeconst Foo) *maybeconst Bar but then you just have anytype wrapped in a fairly niche blanket. There might be existing issues (id guess on github), you might find something more substantial there than what Im saying here.

1 Like

I think that what you’re asking here is tantamount to having the language implicitly infer return type. We can certainly argue about whether this is a good idea, but what you’re describing is already possible in zig just by being explicit about the return type.

const Bar = u8;
const Foo = struct{bar : Bar};

fn getBar(foo : anytype) constp(@TypeOf(foo), Bar) {
    // Add asserts here to ensure foo is *Foo or *const Foo
    return &foo.bar;
}

fn constp(T : type, U : type) type {
    if(@typeInfo(T).pointer.is_const) {
        return *const U;
    } else {
        return *U;
    }
}

test "getBar test" {
    var foo_v = Foo{.bar = 0};
    const foo_c = Foo{.bar = 1};

    const bar_vv : *Bar = getBar(&foo_v); _ = bar_vv; // pass
    const bar_cv : *Bar = getBar(&foo_c); _ = bar_cv; // fail
    const bar_vc : *const Bar = getBar(&foo_v); _ = bar_vc; // pass
    const bar_cc : *const Bar = getBar(&foo_c); _ = bar_cc; // pass
}

It looks like there’s an old github issue discussing return type inference which was eventually discarded.

Is the code-gen smart enough to deduplicate those functions (in case they don’t get inlined)?

std.BoundedArray used to work like this:

Zig Documentation

It has been removed from std, but the structure and the pattern are still useful