Confusion with if and optionals

I have this code:

if (try entry.fields.fetchPut(config.field_name, new_value)) |old| {
    old.value.deinit(allocator);
}

and I get this error:

src/cli/main.zig:876:22: error: expected type '*value.Value', found '*const value.Value'
            old.value.deinit(allocator);
            ~~~~~~~~~^~~~~~~
src/cli/main.zig:876:22: note: cast discards const qualifier
src/core/value.zig:64:25: note: parameter type declared here
    pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {

But the return type of fetchPut is !?KV, and my hash map owns Value: std.StringHashMap(Value) so old should be of type KV and the value field of that is of type Value not *const Value right? Is it the if () || syntax, is old like a const variable, I even tried doing |*old| but that didn’t work either.

What is the solution to this?

1 Like

the problem is that method call syntax that you use in the deinit call can coerce const thing: T to *const T and var thing: T to *T. you should attempt capturing by pointer |*old| so that old is of type *T. if that doesn’t work, you would need to assign var copy = old and call copy.deinit(). That sounds to me like a code smell, but I don’t know what the structure of your Value; if it needs a deinit because it owns some pointers inside, then this is fine.

I was about to suggest that as well, but OP said

The issue is, that the return value of a function is a constant, and capturing a pointer to it will still get you a pointer to constant memory.

I think fetchPut returning ?!*KV instead of ?!KV should solve it.
And I think, zig likely saved you from a bug there :slight_smile:

Edit: thinking about it again, it is probably necessary to assign the return value to a variable instead of returning a pointer, because at that address the new value is when the function returns

Thanks, I had done the assigning to a variable first, but that felt like I was doing extra stuff for nothing so I was looking for a better solution.

I’m still confused about how const works in Zig, how it propagates, and how it affects a type. But I wrote this little test program.

const std = @import("std");

const Foo = struct {
    pub fn deinit(self: *Foo) void {
        _ = self;
    }
};

pub fn main() !void {
    var opt: ?Foo = .{};
    opt = .{}; // here to force var

    // Works.
    opt.?.deinit();

    // Works.
    if (opt) |*f| {
        f.deinit();
    }

    // Doesn't work.
    // if (opt) |f| {
    //     f.deinit();
    // }
    // src/main.zig:17:10: error: expected type '*main.Foo', found '*const main.Foo'
    // src/main.zig:17:10: note: cast discards const qualifier
    // src/main.zig:4:25: note: parameter type declared here

    // ---

    const opt2: ?Foo = .{};

    // Doesn't work.
    // opt2.?.deinit();
    // src/main.zig:30:11: error: expected type '*main.Foo', found '*const main.Foo'
    // src/main.zig:30:11: note: cast discards const qualifier
    // src/main.zig:4:25: note: parameter type declared here

    // Doesn't work.
    // if (opt2) |*f| {
    //     f.deinit();
    // }

    // Doesn't work.
    // if (opt2) |f| {
    //     f.deinit();
    // }
    // src/main.zig:45:10: error: expected type '*main.Foo', found '*const main.Foo'
    // src/main.zig:45:10: note: cast discards const qualifier
    // src/main.zig:4:25: note: parameter type declared here
}

So it seems like the |*val| syntax does sometimes work? It depends if the value was originally declared with var or const?

But KV is desribed as “A copy of a key and value which are no longer in the map” so a pointer wouldn’t make sense

Ah sorry, I was not aware you where talking about a map in std.

var optional = try entry.fields.fetchPut(config.field_name, new_value);
if (optional) |*old| {
    old.value.deinit(allocator);
}

This looks like the part of you code that works, if function return types are constant, we have it assigned to the var optional anyways, the only difference I see is that we are accessing field of old where your function calls the deinit function directly on f

I guess this is the solution code:

if (try entry.fields.fetchPut(config.field_name, new_value)) |kv| {
    // Assign because `old_value` is const
    var old_value = kv.value;
    old_value.deinit(allocator);
}

does this work?

const gop = try entry.fields.getOrPut(config.field_name);
if (gop.found_existing) gop.value_ptr.deinit(allocator);
gop.value_ptr.* = new_value;
2 Likes

Aarg. I’ve seen that syntax once when I was using some random library… I guess I understand it… but I don’t like it.

So the old_value is const, but it stores a pointer. So you copy the pointer value to another variable that isn’t const and now you can modify the thing the pointer points to?

Not necessarily. deinit could be something like:

fn deinit(value: *Value) void {
    value.* = undefined;
}

which tracks “use after deinit” in debug mode. Copying kv.value has incorrect semantics in this case because old_value (the copy) gets set to undefined instead of wherever the original Value is. Using fetchPut also has incorrect semantics

pub fn fetchPutContext(self: *Self, allocator: Allocator, key: K, value: V, ctx: Context) Allocator.Error!?KV {
    const gop = try self.getOrPutContext(allocator, key, ctx);
    var result: ?KV = null;
    if (gop.found_existing) {
        result = KV{
            .key = gop.key_ptr.*,
            .value = gop.value_ptr.*,
        };
    }
    gop.value_ptr.* = value;
    return result;
}

because result contains a copy of the original Value via gop.value_ptr.*. It’s like doing:

const gop = try entry.fields.getOrPut(config.field_name);
if (gop.found_existing) {
    var old_value = gop.value_ptr.*; // copy
    // deinit old_value instead of gop.value_ptr
    old_value.deinit(allocator);

    gop.value_ptr.foo() // doesn't panic
    old_value.foo() // panics
}
gop.value_ptr.* = new_value;

If any container, whether const or var, contains a non-const pointer (e.g. *u32) you are allowed to mutate the pointer contents. If a container is var then you are allowed to mutate the pointer itself e.g.

var x: u32 = 0;
var y: u32 = 2;
const foo = .{.x = &x};
var bar = .{.x = &x};

foo.x.* = 1; // fine
foo.x = &y; // not fine
bar.x.* = 2; // fine
bar.x = &y; // fine

Same goes for variables

var x: u32 = 0;
var y: u32 = 0;
const z: *u32 = &x;
var w: *u32 = &x;

z.* = 1; // fine
z = &y; // not fine
w.* = 1; // fine
w = &y; // fine