Double move init with dictionaries

I’ve encountered unexpected behavior when using custom struct with List and Dict. I’m not sure if this is expected behavior or a bug, but the __moveinit__ method is called twice when used with a Dict, but only once with a List. Here’s the code

@value
struct A(Writable):
    var value: Int32
    fn __moveinit__(out self, owned existing: Self):
        print("move and add 1")
        self.value = existing.value + 1

    fn write_to[W: Writer](self, mut writer: W):
        writer.write('A=', self.value)

def main():
    var a_list = List[A]()
    a_list.append(A(0))
    print(a_list[0])

    var a_dict = Dict[Int, A]()
    a_dict[0] = A(0)
    print(a_dict[0])

Expected output:

move and add 1
A=1
move and add 1
A=1

The output is:

move and add 1
A=1
move and add 1
move and add 1
A=2

Maybe this is because in List.append() the item gets directly moved into the List while in Dict.__setitem__() it is first moved into a DictEntry which is then moved into the Dict.

1 Like

Great catch. This appears to be because of how dict is layered with DictEntry, from dict.mojo:

    fn _insert(mut self, owned key: K, owned value: V):
        self._insert(DictEntry[K, V](key^, value^))  ### MOVE V into DictEntry

    fn _insert[
        safe_context: Bool = False
    ](mut self, owned entry: DictEntry[K, V]):
        @parameter
        if not safe_context:
            self._maybe_resize()
        var found, slot, index = self._find_index(entry.hash, entry.key)

        self._entries[index] = entry^ # MOVE DictEntry (and elements) into storage.

Someone could optimize this extra move away by making these a little more clever, e.g. make the first one the canonical insert. It’s unclear to me why anyone should be inserting DictEntry directly anyway.

2 Likes