I'm confused about the const-ness of while loop captures

Hello friends!

The following does not compile (zig 0.15.2/master), and I am curious why:

pub const Item = struct {
    fn doSomething(_: *Item) void {}
};

pub const Iter = struct {
    fn next(_: *Iter) ?Item {
        return null;
    }
};

pub fn main() void {
    var iter: Iter = .{};

    while (iter.next()) |*i| {
        i.doSomething();
    }
}

The error is:

test.zig:15:10: error: expected type '*test.Item', found '*const test.Item'
        i.doSomething();
        ~^~~~~~~~~~~~
test.zig:15:10: note: cast discards const qualifier
test.zig:2:23: note: parameter type declared here
    fn doSomething(_: *Item) void {}

No matter what I do, I can’t call doSomething on the captured variable.
Surprisingly, the problem persists even if I assign it to an intermediate var. This is the pattern I use when I’d like to mutate function parameters … but it does not work in this context.

    // <snip>
    while (iter.next()) |*i| {
        var tmp = i;
        tmp.doSomething();
    }

Why is this not possible?

I have tried to search for “while const capture” on both the discord and the issue tracker. The closes related thing i found was this issue. … but it doesn’t seem exactly related (granted i’m not sure i understand it)

2 Likes

An iterator returning ?Item means that you are returning a copy of Item, if you want to mutate that item it doesn’t make sense to use an iterator that returns a copy.

A lot of iterators will return you a struct that just contains pointers, like the entry struct that contains pointers to the key and value that the HashMap.iterator returns from the next function. (Or iterators that return ?[]SomeSliceElement are also (optionals of) pointers (with a length))

If you want to mutate the returned item, then your iterator should return an optional pointer to the item, so it would use:

fn next(_: *Iter) ?*Item {
    return null;
}

That means that your loop would be written like this:

while (iter.next()) |ptr| {
    ptr.doSomething();
}

One example of this within the standard library is the reverse iterator, which has next and nextPtr depending on whether you want to use copying or pointer access.

3 Likes

Interesting … in my case, my iterator cannot return a pointer :/. The returned items are constructed in the .next() method on the stack.

An iterator returning ?Item means that you are returning a copy of Item, if you want to mutate that item it doesn’t make sense to use an iterator that returns a copy.

Hmm, but if I call the .next() method normally and put the result in a var, i am able to mutate the result. So it does not feel like this has anything to do with me returning a copy from .next() :thinking:

This compiles:

pub const Item = struct {
    fn doSomething(_: *Item) void {}
};

pub const Iter = struct {
    fn next(_: *Iter) ?Item {
        return null;
    }
};

pub fn main() void {
    var iter: Iter = .{};

    // CHANGED 
    var first = iter.next() orelse return;
    first.doSomething();
}

If this was allowed without putting the result into a var, the potential confusion/footgun is that you would not be modifying the original by doing this, you’re modifying a copy. Having to do the extra step of copying it into a var makes it clear that you are modifying a copy.

1 Like

But as I pointed out in the original post, this does not work in the while loop case. Putting the capture in a var still does not compile.

In the first example, the error is because you’re using * in the capture. Don’t do that, and then you can copy the result into a var.

1 Like

Here *i is capturing the pointer to a temporary which is why you get a *const Item, if you actually want to mutate the copy then you have to either leave out the capture by pointer or dereference the pointer, so either:

while (iter.next()) |*i| {
    var tmp = i.*;
    tmp.doSomething();
}

Or:

while (iter.next()) |i| {
    var tmp = i;
    tmp.doSomething();
}
2 Likes

Ahhhh, I see.
I didn’t realize that the |*i| was semantically different than manually taking a reference. So a pointer to a temporary is always const? That clears things up!

Thank you!

You both answered my question, but i’ll mark Sze’s as the solution because it provides more detail

2 Likes

it isn’t different, manually taking a reference has the same behaviour

this:

while (iter.next()) |*i| {
    var tmp = i;
    tmp.doSomething();
}

is equivalent to:

while (true) {
    var tmp = &iter.next() orelse break;
    tmp.doSomething();
}
2 Likes