2

This is the same as Why does go forbid taking the address of (&) map member, yet allows (&) slice element? but I'm not satisfied with the accepted answer: "Slices are backed by a backing array and maps are not."

Note: I have now added my own answer to the referenced question above.

The question Access Struct in Map (without copying) is even better, but its accepted answer says you can't modify a field of a struct value in a map because you cannot take its address (which is my question).

Maps are backed by memory structures (possibly including arrays) just like slices are.

So what is the real reason why I can't take the address of a map value?

I wanted to modify a map struct value in place. Numeric values in maps can be modified in place using operators like ++ or +=

     func icandothis() {
        cmap := make(map[int]complex64)
        cmap[1] += complex(1, 0)
        fmt.Println(cmap[1])
     }

But struct values cannot be modified:

type Complex struct {
    R float32
    I float32
}

func (x *Complex) Add(c Complex) {
    x.R += c.R
    x.I += c.I
}

func but_i_cannot_do_this() {
    cmap := make(map[int]Complex)
    //cmap[1].Add(Complex{1, 0})
    fmt.Println(cmap[1])

}

func so_i_have_to_do_this() {
    cmap := make(map[int]Complex)
    c := cmap[1]
    c.Add(Complex{1, 0})
    cmap[1] = c
    fmt.Println(cmap[1])

}
user13097
  • 343
  • 3
  • 12
  • 1
    Because maps are free to rearrange their contents on the fly, meaning that after the map is modified, the pointer may no longer point at the same value it did when it was created. – Adrian Mar 09 '18 at 21:02
  • 1
    However, since your whole problem is that your method receiver is of pointer type... why not just store pointers in your map? – Adrian Mar 09 '18 at 21:03
  • 2
    Duplicate. Too lazy to search. – Volker Mar 09 '18 at 21:31
  • I know the question is a duplicate. I mention this in the question. The point is that I was not satisfied with the accepted answers in the original questions, so asked it again with my objections to the previous answers. – user13097 Mar 11 '18 at 11:10
  • @Adrian, I think that your first comment is the correct answer: A map can rearrange its content on the fly, contrary to a slice that never does that. A slice never grows. So probably the designers of the language felt it would be safer for the programmers not to have a pointer to a value that could change at a time not of their choosing, or to give the map implementation freedom to reuse the relocated memory for something other than a value of the same type. – user13097 Mar 11 '18 at 11:21

2 Answers2

16

Let's start with this misconception:

I wanted to modify a map struct value in place. Numeric values in maps can be modified in place using operators like ++ or +=

 func icandothis() {
    cmap := make(map[int]complex64)
    cmap[1] += complex(1, 0)
    fmt.Println(cmap[1])
 }

Let's expand the shorthand form:

package main

import (
    "fmt"
)

func icandothisShort() {
    cmap := make(map[int]complex64)
    cmap[1] += complex(1, 0)
    fmt.Println(cmap[1])
}

func icandothisLong() {
    cmap := make(map[int]complex64)
    // An assignment operation x op= y where op is a binary arithmetic operator
    // is equivalent to x = x op (y) but evaluates x only once.
    // cmap[1] += complex(1, 0)
    v := cmap[1]          // v = zero value = complex(0, 0)
    v = v + complex(1, 0) // v = complex(0, 0) + complex(1, 0) = complex(1, 0)
    cmap[1] = v           // cmap[1] = v = complex(1, 0)
    a := cmap[1]          // a = complex(1, 0)
    fmt.Println(a)        // complex(1, 0)
}

func main() {
    icandothisShort()
    icandothisLong()
}

Playground: https://play.golang.org/p/1OgmI_AD9uN

Output:

(1+0i)
(1+0i)

As you can see in icandothisLong(), the expanded form of icandothisShort(), there is no update in-place.



The next misconception,

Maps are backed by memory structures (possibly including arrays) just like slices are.

So what is the real reason why I can't take the address of a map value?


Maps are backed by bucket memory structures. A map key, through an imperfect, dynamic hash, identifies a current primary bucket. The map keys and values are stored in the primary bucket or an overflow bucket. The map buckets are constantly reorganized as map entries are created, updated, and deleted. A map entry has no fixed location in memory.

Here is a fairly detailed explanation of how maps work:

GopherCon 2016: Keith Randall - Inside the Map Implementation

Alex B
  • 82,554
  • 44
  • 203
  • 280
peterSO
  • 158,998
  • 31
  • 281
  • 276
  • 1
    Thanks @peterSO your statement that `It is a false claim` is absolutely right. An upvote – Himanshu Mar 09 '18 at 21:51
  • 2
    I am corrected in that cmap[1] += complex(1,0) does not "modify" the map element in place, but a good compiler implementation should not evaluate cmap[1] twice, just as the specification says. As for my ignorance of map implementation, and even more so for the garbage collection implementation, I will not accept my lack of knowledge as an answer. – user13097 Mar 11 '18 at 08:03
  • Thanks for the video of the map implementor, who answers the question (partially): "It interferes with this growing procedure, so if I take the address of some entry in the bucket, and then I keep that entry around for a long time and in the meantime the map grows, then all of a sudden that pointer points to an old bucket and not a new bucket and that pointer is now invalid, so it's hard to provide the ability to take the address of a value in a map, without constraining how grow works... C++ grows in a different way, so you can take the address of a bucket" (21:45) – user13097 Mar 11 '18 at 11:26
  • I accepted this answer because of the reference to the talk by the map implementor. – user13097 Mar 13 '18 at 07:06
1

Because an alternative would be to not have delete function. Consider a following example.

m := map[int]int{1: 2}
v := &m[1]
delete(m, 1)

What v points to?

There are four possible answers (well, more, but they are just as bad), none of which are satisfactory.

  • A tombstone marking an missing entry. This wouldn't allow for reusing an entry after it was removed from a hash table which would require more common resizes and would waste memory in maps where elements are often deleted.

  • A dangling pointer, which is incompatible with Go memory safety requirements.

  • This is an invalid code. Would require runtime checking for all pointer accesses or implementing Rust style borrow checker.

  • Require values to be behind a pointer which adds an indirection. You can do it yourself by using types like map[int]*int.

You may say that a program shouldn't access pointers to a map element after it was deleted. This would be fine in memory unsafe programming language, which Go isn't.

By the way, for reference purposes, other map operations are definitely possible to implement while allowing to take a reference to a map element. A map implementation following C++ iterator invalidation requirements will get those easily - although there is a cost as C++ maps are relatively slow. But if most C++ programs can manage this cost, so can Go.

Konrad Borowski
  • 11,584
  • 3
  • 57
  • 71
  • A pointer to a deleted value in a map should point to whatever valid value is stored at that location at the time, possibly the zero value. It doesn't have to be the original value. Just like a pointer to a slice element may point to something other than the original value if the element has been replaced, by setting it to something else. – user13097 Mar 11 '18 at 07:50