Equality of interface of an empty struct - why?

706 views
Skip to first unread message

Brien Colwell

unread,
Feb 22, 2024, 5:55:49 AMFeb 22
to golang-nuts
I'm confused by this output. It appears that the interface of two different pointers to an empty struct are equal. In all other cases, interface equality seems to be the pointer equality. What's going on in the empty struct case?

```
package main

import "fmt"

type Foo struct {
}

func (self *Foo) Hello() {
}

type FooWithValue struct {
A int
}

func (self *FooWithValue) Hello() {
}

type Bar interface {
Hello()
}

func main() {
a := &Foo{}
b := &Foo{}
fmt.Printf("%t\n", *a == *b)
fmt.Printf("%t\n", a == b)
fmt.Printf("%t\n", Bar(a) == Bar(b))

c := &FooWithValue{A: 1}
d := &FooWithValue{A: 1}
fmt.Printf("%t\n", *c == *d)
fmt.Printf("%t\n", c == d)
fmt.Printf("%t\n", Bar(c) == Bar(d))
}
```

Prints (emphasis added on the strange case):

```
true
false
**true**
true
false
false
```


Axel Wagner

unread,
Feb 22, 2024, 6:01:31 AMFeb 22
to Brien Colwell, golang-nuts

> A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/d93760c9-61a7-4a3c-9b5c-d89f023d2253n%40googlegroups.com.

burak serdar

unread,
Feb 22, 2024, 10:53:41 AMFeb 22
to Brien Colwell, golang-nuts
The compiler can allocate the same address for empty structs, so I
actually expected a==b to be true, not false. However, there's
something more interesting going on here because:

a := &Foo{}
b := &Foo{}
fmt.Printf("%t\n", *a == *b)
fmt.Printf("%t\n", a == b)
fmt.Printf("%p %p\n", a, b)
x := Bar(a)
y := Bar(b)
fmt.Printf("%t\n", Bar(a) == Bar(b))
fmt.Printf("%t\n", x == y)

Prints:

true
true
0x58e360 0x58e360 // Note that a and be are pointing to the same address
true
true


But:
a := &Foo{}
b := &Foo{}
fmt.Printf("%t\n", *a == *b)
fmt.Printf("%t\n", a == b)
//fmt.Printf("%p %p\n", a, b) // Comment out the print
x := Bar(a)
y := Bar(b)
fmt.Printf("%t\n", Bar(a) == Bar(b))
fmt.Printf("%t\n", x == y)


Prints:

true
false
true
true

Axel Wagner

unread,
Feb 22, 2024, 11:00:47 AMFeb 22
to burak serdar, Brien Colwell, golang-nuts
Note that in the Spec section I quoted above it says "Two distinct zero-size variables may have the same address in memory" (emphasis mine).
There is no guarantee, that all zero-sized values have the same address (otherwise, you'd get into inefficiencies when taking the address of a zero-sized field in a larger struct, or when converting a zero-capacity slice into an array-pointer). But it is allowed.
If you require two pointers returned from different code paths to be different, for correctness, then you have to make them point at something that has non-zero size. Otherwise, all potential combinations are valid according to the spec.

Axel Wagner

unread,
Feb 22, 2024, 11:09:21 AMFeb 22
to burak serdar, Brien Colwell, golang-nuts
FWIW my guess about your example is that the printing causes the pointers to escape (as they are put into an interface), which allocates them on the heap, triggering the "allocating zero-sized values returns the same address" optimization in the runtime. Removing the print causes them to instead be put on the stack, where they end up with different addresses, as you basically get the stack layout of `[struct{}, *struct{}, struct{}, *struct{}]` - that is, `a` and `b` are pointers and need to be stored on the stack, but then point at zero-sized stack variable.

But that's a guess and as I said, either way it is essentially random - the behavior is not guaranteed by the spec either way.

On Thu, Feb 22, 2024 at 4:53 PM burak serdar <bse...@computer.org> wrote:

burak serdar

unread,
Feb 22, 2024, 11:12:31 AMFeb 22
to Axel Wagner, Brien Colwell, golang-nuts
On Thu, Feb 22, 2024 at 9:00 AM Axel Wagner
<axel.wa...@googlemail.com> wrote:
>
> Note that in the Spec section I quoted above it says "Two distinct zero-size variables may have the same address in memory" (emphasis mine).
> There is no guarantee, that all zero-sized values have the same address (otherwise, you'd get into inefficiencies when taking the address of a zero-sized field in a larger struct, or when converting a zero-capacity slice into an array-pointer). But it is allowed.
> If you require two pointers returned from different code paths to be different, for correctness, then you have to make them point at something that has non-zero size. Otherwise, all potential combinations are valid according to the spec.

Yes. But in that case, you'd expect either a==b and Bar(a)==Bar(b) to
be both true, or both false. In this case, one is true and the other
is not.

Axel Wagner

unread,
Feb 22, 2024, 11:15:47 AMFeb 22
to burak serdar, Brien Colwell, golang-nuts
If you expect that, you are misreading the spec. There is no guarantee of any behavior here. An implementation is allowed to flip a coin, every time you create a pointer to a zero-sized variable, and either return a unique pointer or a singleton. I think you may assume that &a == &a, always. But apart from that, who knows.

Zero-sized variables *may* have the same address. They don't *have* to.

burak serdar

unread,
Feb 22, 2024, 11:21:07 AMFeb 22
to Axel Wagner, Brien Colwell, golang-nuts
Creating an interface is not creating a pointer to a zero sized variable.

a==b prints false. That means, a and b point to different locations
Bar(a)==Bar(b) prints true. If a!=b, then Bar(a) must be different from Bar(b)

On Thu, Feb 22, 2024 at 9:15 AM Axel Wagner

Axel Wagner

unread,
Feb 22, 2024, 11:47:42 AMFeb 22
to burak serdar, Brien Colwell, golang-nuts
I see. Sorry, I was jumping to conclusions and didn't quite get what you mean. That is my fault.

I agree that this looks confusing and is arguably a bug. I filed https://github.com/golang/go/issues/65878, thanks for pointing it out.

Axel Wagner

unread,
Feb 22, 2024, 11:50:38 AMFeb 22
to burak serdar, Brien Colwell, golang-nuts
Hm actually, the spec allows for this, technically speaking: https://go.dev/ref/spec#Comparison_operators

> Pointers to distinct zero-size variables may or may not be equal.

Arguably, this genuinely would allow comparison of pointers to zero-sized variables to have any behavior whatsoever (including being random). But it certainly is confusing.

burak serdar

unread,
Feb 22, 2024, 12:06:35 PMFeb 22
to Axel Wagner, Brien Colwell, golang-nuts
I don't think this case really applies here. I get that comparison of
a==b may or may not be true. The problem is that if a==b at some point
in a program, it should be the case that a==b for all other cases in
that same program. That is, if a==b, then
interface{}(a)==interface{}(b), and vice versa. But what we have here
is a!=b but interface{}(a)==interface{}(b)

On Thu, Feb 22, 2024 at 9:50 AM Axel Wagner

Axel Wagner

unread,
Feb 22, 2024, 12:39:59 PMFeb 22
to burak serdar, Brien Colwell, golang-nuts
On Thu, Feb 22, 2024 at 6:06 PM burak serdar <bse...@computer.org> wrote:
I don't think this case really applies here. I get that comparison of
a==b may or may not be true. The problem is that if a==b at some point
in a program, it should be the case that a==b for all other cases in
that same program.

Why? I mean, I get that it makes sense intuitively, but how does this follow from the spec? That sentence says "a comparison of two pointers to zero sized values may or may not be true". It does not qualify that statement in any way.

burak serdar

unread,
Feb 22, 2024, 12:44:41 PMFeb 22
to Axel Wagner, Brien Colwell, golang-nuts
On Thu, Feb 22, 2024 at 10:39 AM Axel Wagner
<axel.wa...@googlemail.com> wrote:
>
>
>
> On Thu, Feb 22, 2024 at 6:06 PM burak serdar <bse...@computer.org> wrote:
>>
>> I don't think this case really applies here. I get that comparison of
>> a==b may or may not be true. The problem is that if a==b at some point
>> in a program, it should be the case that a==b for all other cases in
>> that same program.
>
>
> Why? I mean, I get that it makes sense intuitively, but how does this follow from the spec? That sentence says "a comparison of two pointers to zero sized values may or may not be true". It does not qualify that statement in any way.

We are comparing two interfaces containing pointers to zero-size
structs. If those pointers are not equal, then the interfaces should
not be equal as well.

Maybe the spec should be clarified to say "for a compilation of a
program, two pointers to zero-size variables may or may not be equal",
because otherwise it implies that if you have

x:= a==b
y:= a==b

x may or may not be true. If a==b, then that should hold for every
execution of that program, and throughout the program.

burak serdar

unread,
Feb 22, 2024, 12:48:08 PMFeb 22
to Axel Wagner, Brien Colwell, golang-nuts
On Thu, Feb 22, 2024 at 10:44 AM burak serdar <bse...@computer.org> wrote:
>
> On Thu, Feb 22, 2024 at 10:39 AM Axel Wagner
> <axel.wa...@googlemail.com> wrote:
> >
> >
> >
> > On Thu, Feb 22, 2024 at 6:06 PM burak serdar <bse...@computer.org> wrote:
> >>
> >> I don't think this case really applies here. I get that comparison of
> >> a==b may or may not be true. The problem is that if a==b at some point
> >> in a program, it should be the case that a==b for all other cases in
> >> that same program.
> >
> >
> > Why? I mean, I get that it makes sense intuitively, but how does this follow from the spec? That sentence says "a comparison of two pointers to zero sized values may or may not be true". It does not qualify that statement in any way.
>
> We are comparing two interfaces containing pointers to zero-size
> structs. If those pointers are not equal, then the interfaces should
> not be equal as well.
>
> Maybe the spec should be clarified to say "for a compilation of a
> program, two pointers to zero-size variables may or may not be equal",
> because otherwise it implies that if you have
>
> x:= a==b
> y:= a==b
>

I mistyped here, so to clarify: for the above program, x must always
be equal to y, even if a and b are pointers to zero-size values.

Axel Wagner

unread,
Feb 22, 2024, 1:07:08 PMFeb 22
to burak serdar, Brien Colwell, golang-nuts
On Thu, Feb 22, 2024 at 6:44 PM burak serdar <bse...@computer.org> wrote:
Maybe the spec should be clarified to say "for a compilation of a
program, two pointers to zero-size variables may or may not be equal",
because otherwise it implies that if you have

x:= a==b
y:= a==b

x may or may not be true.

Well, given that the spec is *not* saying what you say it maybe should - it seems we are in agreement. It is indeed correct for `x` to may or not be equal to `y` here, with the spec as it is right now.

jake...@gmail.com

unread,
Feb 24, 2024, 11:28:07 AMFeb 24
to golang-nuts
What is really fantastical is that a==b prints false, even though the pointers are actually the same. I am guessing some sort of optimization effect is at play here.


type Foo struct {

}

func main() {

a := &Foo{}
b := &Foo{}
fmt.Printf("%t\n", *a == *b)
fmt.Printf("%t\n", a == b)
q := uintptr(unsafe.Pointer(a))
r := uintptr(unsafe.Pointer(b))

//fmt.Printf("%p %p\n", a, b)
fmt.Printf("%t\n", q == r)
fmt.Printf("%x %x\n", q, r)
}

prints:

true 
false 
true 
c000104ee0 c000104ee0

wild! (or am I missing something?)

Axel Wagner

unread,
Feb 24, 2024, 2:43:18 PMFeb 24
to jake...@gmail.com, golang-nuts
FWIW I believe there is enough subtlety here (small changes in the code might trigger different compiler optimizations) that I wouldn't rely too much on probing the compiler with different programs. Instead, I'd suggest decompiling the binary and/or running it in a debugger, to check what the actual pointers are. From looking at godbolt, AIUI the compiler is optimizing the comparison into a constant: https://go.godbolt.org/z/x1Ef3PxPb

Though, really, I don't think this is *super* weird. Like, don't get me wrong, the behavior is counter-intuitive. But once you've accepted that "comparison of pointers to zero-sized variables is not really defined", the actual myriads of ways in which it behaves counter-intuitively become less important.



tapi...@gmail.com

unread,
Feb 25, 2024, 3:30:22 AMFeb 25
to golang-nuts
Absolutely a bug.

tapi...@gmail.com

unread,
Feb 25, 2024, 4:02:20 AMFeb 25
to golang-nuts
The behavior of Go 1.9 or 1.10 is even more weird.
They make the following code print false. ;D

package main

type T struct {}

func main() {
  var a, b = &T{}, &T{}
  println(a == b || a != b)
}

brien colwell

unread,
Feb 26, 2024, 4:39:16 AMFeb 26
to tapi...@gmail.com, golang-nuts
I learned a lot from this thread, thank you. 

Intuitively the spec seems to conclude a pointer to an empty struct is a different type of pointer? Normally a pointer wouldn't be able to change values during execution, so we can do things like key maps by pointers. But if every evaluation of the empty struct pointer can lead to a different outcome, isn't that the same as the address of the pointer arbitrarily changing? Otherwise if two pointers are different, then the interface comparison of two pointers must be different also since the pointer address does not change?


On Feb 25, 2024, at 1:02 AM, tapi...@gmail.com <tapi...@gmail.com> wrote:


--
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/JBVqWYFdtC4/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/e4429119-5c91-44d1-93c4-dc877efdd7b9n%40googlegroups.com.

Axel Wagner

unread,
Feb 26, 2024, 4:55:48 AMFeb 26
to brien colwell, tapi...@gmail.com, golang-nuts
I think you should still wait for the outcome of that issue.

On Mon, Feb 26, 2024 at 10:39 AM brien colwell <xcol...@gmail.com> wrote:
I learned a lot from this thread, thank you. 

Intuitively the spec seems to conclude a pointer to an empty struct is a different type of pointer? Normally a pointer wouldn't be able to change values during execution, so we can do things like key maps by pointers.

Note that this is not quite right. It is allowed for addresses to change. It is possible to implement Go with a moving GC, for example, by design. Pointers in a map would have to be updated by the GC in that case.
 
But if every evaluation of the empty struct pointer can lead to a different outcome, isn't that the same as the address of the pointer arbitrarily changing?

Not *quite*. For example, you should be able to rely on `a == a` to always be true (the spec says "distinct variables", so two pointers to non-distinct variables would still have to compare equal). So, if you just pass around a pointer to a single-variable, that should always have the same behavior: https://go.dev/play/p/5nmqwnCiq9L

But it means that if you have two distinct variables of zero size, you can no longer meaningfully talk about whether they are "the same pointer". They might be, or they might not be.

In a sense, it would be valid for `==` on pointers to zero-sized types to always evaluate to `true`, but it wouldn't be valid for them to always evaluate to `false`. There are cases where you can rely on two pointers being the same, but you can *never* rely on them being *different*.

Genereally, though, I'd argue that it's safest to just not assume anything about pointers to zero-sized variables.

You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/96D5EEE6-5E5B-4064-BAA3-AD8B985FE1F0%40gmail.com.

tapi...@gmail.com

unread,
Feb 26, 2024, 12:24:33 PMFeb 26
to golang-nuts
package main

var a, b [0]int
var p, q = &a, &b

func main() {
if (p == q) {
p, q = &a, &b
println(p == q) // false
}
}

On Thursday, February 22, 2024 at 6:55:49 PM UTC+8 Brien Colwell wrote:

Brien Colwell

unread,
Feb 26, 2024, 1:25:46 PMFeb 26
to tapi... @gmail. com, golang-nuts
Interesting. That seems to break the comparable spec.

>> Pointer types are comparable. Two pointer values are equal if they point to the same variable or if both have value nil. Pointers to distinct zero-size variables may or may not be equal.


>> it would be valid for `==` on pointers to zero-sized types to always evaluate to `true`,

This would be more straightforward behavior.



-- 
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/JBVqWYFdtC4/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.

Axel Wagner

unread,
Feb 26, 2024, 4:16:30 PMFeb 26
to Brien Colwell, tapi... @gmail. com, golang-nuts
On Mon, Feb 26, 2024 at 7:25 PM Brien Colwell <xcol...@gmail.com> wrote:
Interesting. That seems to break the comparable spec.

>> Pointer types are comparable. Two pointer values are equal if they point to the same variable or if both have value nil. Pointers to distinct zero-size variables may or may not be equal.

How do you think this is broken? Note that there are two distinct variables involved (a and b). Pointers to distinct variables are allowed to be equal, or not to be equal.
 
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/E875D5D3-FFB2-40BA-B930-A10461A2998E%40gmail.com.

Brien Colwell

unread,
Feb 26, 2024, 4:20:48 PMFeb 26
to Axel Wagner, tapi... @gmail. com, golang-nuts
>> Note that there are two distinct variables involved (a and b).

You’re right. I misread this.

tapi...@gmail.com

unread,
Feb 27, 2024, 12:20:18 AMFeb 27
to golang-nuts
On Tuesday, February 27, 2024 at 2:25:46 AM UTC+8 Brien Colwell wrote:
Interesting. That seems to break the comparable spec.

>> Pointer types are comparable. Two pointer values are equal if they point to the same variable or if both have value nil. Pointers to distinct zero-size variables may or may not be equal.


From common sense, this is an obvious bug. But the spec is indeed not clear enough.
It doesn't state whether or not comparisons of pointers to two distinct zero-size variables should be consistent in a run session.
Though, from common sense, it should.

Jan Mercl

unread,
Feb 27, 2024, 2:42:25 AMFeb 27
to tapi...@gmail.com, golang-nuts
On Tue, Feb 27, 2024 at 6:20 AM tapi...@gmail.com <tapi...@gmail.com> wrote:

> From common sense, this is an obvious bug. But the spec is indeed not clear enough.
> It doesn't state whether or not comparisons of pointers to two distinct zero-size variables should be consistent in a run session.
> Though, from common sense, it should.

"Pointers to distinct zero-size variables may or may not be equal."

The outcome is specified to be not predictable. Expecting consistency
means the outcome should be, or eventually become, predictable. That's
the opposite of what the specs say.

tapi...@gmail.com

unread,
Feb 27, 2024, 2:52:35 AMFeb 27
to golang-nuts
Then I would argue that the spec never guarantee (x != y) == ! (x == y).
for values of any types (including non-zero-size types). :D

Kurtis Rader

unread,
Feb 27, 2024, 3:10:16 AMFeb 27
to tapi...@gmail.com, golang-nuts
The spec is reasonably clear that guarantee does apply to pointers to non-zero size variables (e.g., non-empty structs). The issue in this discussion thread is limited to the handling of pointers to zero size variables. Precisely because of optimizations the compiler may, or may not, perform regarding the value of such pointers. I would prefer a stronger guarantee regarding pointers to zero size variables, and it looks like a lot of Go users agree with you and me, but that doesn't mean the current behavior is ipso facto broken or useless. Pointers to zero size variables (e.g., empty structs) are a special-case that the Go designers decided to leave ambiguous for now.

--
Kurtis Rader
Caretaker of the exceptional canines Junior and Hank

Marvin Renich

unread,
Feb 27, 2024, 2:19:10 PMFeb 27
to golan...@googlegroups.com
* Kurtis Rader <kra...@skepticism.us> [240227 03:10]:
> On Mon, Feb 26, 2024 at 11:52 PM tapi...@gmail.com <tapi...@gmail.com>
> wrote:
>
> > On Tuesday, February 27, 2024 at 3:42:25 PM UTC+8 Jan Mercl wrote:
> >
> > On Tue, Feb 27, 2024 at 6:20 AM tapi...@gmail.com <tapi...@gmail.com>
> > wrote:
> >
> > > From common sense, this is an obvious bug. But the spec is indeed not
> > clear enough.
> > > It doesn't state whether or not comparisons of pointers to two distinct
> > zero-size variables should be consistent in a run session.
> > > Though, from common sense, it should.
> >
> > "Pointers to distinct zero-size variables may or may not be equal."
> >
> > The outcome is specified to be not predictable. Expecting consistency
> > means the outcome should be, or eventually become, predictable. That's
> > the opposite of what the specs say.
> >
> >
> > Then I would argue that the spec never guarantee (x != y) == ! (x == y).
> > for values of any types (including non-zero-size types). :D
> >
>
> The spec is reasonably clear that guarantee does apply to pointers to
> non-zero size variables (e.g., non-empty structs). The issue in this
> discussion thread is limited to the handling of pointers to zero size
> variables.

I interpreted this thread as being about the inconsistency between the
way pointers to zero-size variables were compared vs. non-zero-size
variables. His comment looks perfectly on-topic to me.

> Precisely because of optimizations the compiler may, or may not,
> perform regarding the value of such pointers. I would prefer a stronger
> guarantee regarding pointers to zero size variables, and it looks like a
> lot of Go users agree with you and me, but that doesn't mean the current
> behavior is ipso facto broken or useless. Pointers to zero size variables
> (e.g., empty structs) are a special-case that the Go designers decided to
> leave ambiguous for now.

I have to agree with tapir. Prior to generics, the type of the
arguments to == were easily known to the programmer, and so it was
obvious when this "undefined" exception would raise its ugly head, and
you just didn't use it for empty struct types. But now, with generics,
this can only be classified as a glaring BUG in the spec. How can a
programmer count on x == y having any meaning at all in code like this:

func IsEqual[T comparable](x, y T) bool {
return x == y
}

if the definition of == for empty structs is undefined? And especially
if the result is different depending on whether or not code outside this
function has called &z for z passed as an argument?
https://gotipplay.golang.org/p/ymhsDmJWv8l

If we can at least agree that this ambiguity is no longer desirable,
let's consider the two possible definitions:

1. Pointers to distinct zero-size variables are equal:

This allows the compiler to easily optimize virtual address usage, but
is inconsistent with the non-zero-size definition.

2. Pointers to distinct zero-size variables are not equal:

This is consistent with the non-zero-size definition, but the
implementation would likely require distinct virtual addresses for
distinct variables. Whether this would require committed memory
corresponding to those virtual addresses is unclear to me.

Definition 1 removes the distinction between empty struct values and
empty struct instances, and the only way for the programmer to get that
distinction back is by using a non-empty struct.

On the other hand, definition 2 preserves the distinction. If a
programmer wants to have instances compare as equal, it is often very
easy to use instances of the empty type rather than instances of a
pointer to the empty type. Implement the methods on the type with value
receivers rather than pointer receivers.

...Marvin

Axel Wagner

unread,
Feb 27, 2024, 6:07:11 PMFeb 27
to golan...@googlegroups.com
On Tue, Feb 27, 2024 at 8:19 PM Marvin Renich <mr...@renich.org> wrote:
Prior to generics, the type of the
arguments to == were easily known to the programmer, and so it was
obvious when this "undefined" exception would raise its ugly head, and
you just didn't use it for empty struct types.  But now, with generics,
this can only be classified as a glaring BUG in the spec.

There is pretty much a 0% chance that we'd change the spec in this regard, at this point. It would mean that variable declarations like `[1<<30]struct{}` would have to allocate huge chunks of heap, to ensure that different index-expressions can have different addresses. And while there shouldn't be any code relying on that not happening for correctness, there is definitely code out there relying on it for performance (e.g. there is a pattern of adding struct fields like `_ [0]func()` to ensure a type is not comparable - such a struct would now change alignment and size).

The optimization that variables of zero size can re-use the same address has been in Go since before Go 1. Given this, it is pretty much implied that comparison of those pointers will sometimes have weird results - the only question is, *which* results are weird. I agree that this is one of the weirder cases. But I don't think we can practically define `==` for pointers to zero-sized variables.

I'll also point out that for generics specifically, I'm not sure *any* action would have a practical effect. If the type argument is not statically known, we also can't special-case it to take into account that it's a pointer to a zero-sized variable. Note that the triggered optimization isn't necessarily "these are pointers to zero-sized variables, hence I can do whatever I want" - it's "these are pointers to distinct variables, hence I can assume they are unequal". That is a generally useful optimization and it would still be applied to generic code.

How can a programmer count on x == y having any meaning at all in code like this:

func IsEqual[T comparable](x, y T) bool {
    return x == y
}

if the definition of == for empty structs is undefined?

The result is defined for empty structs, just not for *pointers* to empty structs.
Note that `==` has other edge-cases as well. In particular, for floating point/complex type arguments, `==` is irreflexive (e.g. NaN is unequal to itself).
I'm not sure that pointers to zero-sized variables make this significantly worse.
 
If we can at least agree that this ambiguity is no longer desirable,

I don't think we can agree on that, sorry.
 
let's consider the two possible definitions:

1. Pointers to distinct zero-size variables are equal:

This allows the compiler to easily optimize virtual address usage, but
is inconsistent with the non-zero-size definition.

Please look at the issue I filed for some discussion of edge-cases we are unlikely to be able to cover satisfactorily. One obvious case is when converting them to `unsafe.Pointer`, in which case the compiler no longer knows that they point at zero-sized variables. Potentially, any such conversion would have to re-assign them the magic "zero-sized variable" address, which then would potentially lead to other weird comparison implications, when code assumes that two `unsafe.Pointer` pointing at distinct variables should have distinct addresses.

We could probably make *more* such comparisons evaluate to `true`, but it's unlikely that we could ever cover *all* of them. It would potentially have prohibitive performance-impact on slicing operations, for example.

2. Pointers to distinct zero-size variables are not equal:

This is consistent with the non-zero-size definition, but the
implementation would likely require distinct virtual addresses for
distinct variables.  Whether this would require committed memory
corresponding to those virtual addresses is unclear to me.

I believe it would. In effect, `struct{}` would have to take at least one byte (as would a zero-sized array).


Definition 1 removes the distinction between empty struct values and
empty struct instances, and the only way for the programmer to get that
distinction back is by using a non-empty struct.

On the other hand, definition 2 preserves the distinction.  If a
programmer wants to have instances compare as equal, it is often very
easy to use instances of the empty type rather than instances of a
pointer to the empty type.  Implement the methods on the type with value
receivers rather than pointer receivers.

I think if these arguments hold any water, the argument "the programmer just shouldn't use pointers to zero-sized variables, if they want defined semantics for ==" is just as valid. That is, if they have control over the type and we are willing to force them to make a decision aligning with our definition, why not force them to make a decision aligning with there not being a definition?
 

...Marvin

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/Zd41i3rHLskqBuef%40basil.wdw.

Brien Colwell

unread,
Feb 27, 2024, 7:17:24 PMFeb 27
to Axel Wagner, golang-nuts
I think the surprising part is that the comparison result can change for the same values because of the assumption that pointers never change. This is implied by the spec but easy to miss.

"Pointers to distinct zero-size variables may or may not be equal."
"Pointers to distinct zero-size variables may or may not be equal and the results may or may not be repeatable in any context."

Agree once a programmer is aware of the behavior it can be avoided.

Best,
Brien


You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/JBVqWYFdtC4/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAEkBMfHVuxSduyWHG20DjzG1jvE0P06fEqC_FyHdDEWewjOjTg%40mail.gmail.com.

Brian Candler

unread,
Feb 28, 2024, 2:06:26 AMFeb 28
to golang-nuts
> let's consider the two possible definitions:
>
> 1. Pointers to distinct zero-size variables are equal: [...]

> 2. Pointers to distinct zero-size variables are not equal:

Another possibility:

3. Equality comparisons between pointers to zero-size variables are forbidden at compile time.
3a. If you wrap two such values in interfaces and try to compare them, then you get a runtime panic, same as certain cases today.

Indeed, what if it were forbidden to take a pointer to a zero-sized variable in the first place? There is nothing to point at, after all.

Axel Wagner

unread,
Feb 28, 2024, 2:19:37 AMFeb 28
to Brian Candler, golang-nuts
That would break backwards compatibility, though. And it would be a re-definition (i.e. existing code would compile, but behave differently at runtime) and is hence not allowed even under the Go 2 transition rules.
I'm also not sure you can exclude *all* pointers to zero-sized variables. Note that `[0]T` is also zero-sized and you can convert slices (even empty ones) into array-pointers. And you can take the address of struct fields.

All of this to solve an honestly pretty small issue. It's a corner, yes. But it isn't a particularly sharp corner.

tapi...@gmail.com

unread,
Feb 29, 2024, 1:02:55 AMFeb 29
to golang-nuts
On Wednesday, February 28, 2024 at 3:19:37 PM UTC+8 Axel Wagner wrote:
That would break backwards compatibility, though. And it would be a re-definition (i.e. existing code would compile, but behave differently at runtime) and is hence not allowed even under the Go 2 transition rules.

With Go version specified, nothing can be broken. For example, the loop var change in Go 1.22 doesn't break backwards compatibility. (Though this is not my opinion, ;D)

Axel Wagner

unread,
Feb 29, 2024, 2:14:40 AMFeb 29
to tapi...@gmail.com, golang-nuts
The loop var change *does* break compatibility. And it did so knowingly and - as clearly explained - was an exception.
Stop arguing in bad faith.

tapi...@gmail.com

unread,
Feb 29, 2024, 4:24:09 AMFeb 29
to golang-nuts
On Thursday, February 29, 2024 at 3:14:40 PM UTC+8 Axel Wagner wrote:
The loop var change *does* break compatibility. And it did so knowingly and - as clearly explained - was an exception.
Stop arguing in bad faith.

An exception.
:D

Axel Wagner

unread,
Feb 29, 2024, 6:04:14 AMFeb 29
to tapi...@gmail.com, golang-nuts
Yes. It means "violating a rule, without setting a precedent for future violations".

One of the main objections to the loop variable change has been the breaking of compatibility and the fear of setting a precedent for that happening in the future. The design doc for it had this to say:

In the Go 2 transitions document we gave the general rule that language redefinitions like what we just described are not permitted, giving this very proposal as an example of something that violates the general rule. We still believe that that is the right general rule, but we have come to also believe that the for loop variable case is strong enough to motivate a one-time exception to that rule. Loop variables being per-loop instead of per-iteration is the only design decision we know of in Go that makes programs incorrect more often than it makes them correct. Since it is the only such design decision, we do not see any plausible candidates for additional exceptions.

This is making it abundantly clear, that the loop var change is a one-time exception, because there is a strong case for it. And that it does not set a precedent for further such changes. When this concern was brought up repeatedly, that was my (and other's) response as well.

So, if nothing else, doing another one of these would lose considerable good-will and trust with the community, because it would mean that our promise that this was a one-time exception was a lie.

Now, you and me both where part of that discussion. And at the time you made an argument, among other things, based on compatibility. e.g. in this comment

That is two surprises for many people. Several people said the two cases are artificial or "intentionally confusing programs". I'm speechless on such non-serious attitudes. I can't believe that the bar to keep backward-compatibility is down to such a low level.
 

If backward-compatibility and expectation breaking is not relevant, then okay, it is not relevant.
 
So, I find the fact that you are now trying to establish the loop variable change as precedent for breaking compatibility - despite knowing full well, that it came with a promise not to do it again - pretty frustrating. It seems to me that, because you did not get your will at the time, you are now bringing it up to troll unrelated conversations. With no actually coherent position.

And so your "one-sentence emoticon" message is neither charming, clever, witty nor funny. It's simply disrespectful.

tapi...@gmail.com

unread,
Feb 29, 2024, 7:02:56 AMFeb 29
to golang-nuts
Reply all
Reply to author
Forward
0 new messages