Comparison of pointers to distinct zero-sized variables

294 views
Skip to first unread message

Oliver Eikemeier

unread,
Jun 19, 2024, 12:59:43 PM (11 days ago) Jun 19
to golang-nuts

I'm observing some strange behavior and could use some help understanding it.

The specification says: “Pointers to distinct zero-size variables may or may not be equal.”

With the following program (Go Playground):

var ( a struct{} b struct{} eq = &a == &b ) func f1() { println("&a:", &a) println("&b:", &b) println("&a == &b:", eq) }


I'll get

&a: 0x537580 &b: 0x537580 &a == &b: false


Okay, a and b are empty structs, do not escape, so they share the same address - fine. Also, some optimizer sees that a and b are different variables, so their addresses must be different, and decides to make &a == &b a constant - wrong, but I can live with that.

My question would be: Is this behavior expected, somehow defined by the specification, or is it undefined behavior?

Let's try to confuse the optimizer a little (Go Playground):

var ( a struct{} b struct{} aa = &a ba = &b eq = aa == ba ) func f1() { println("&a:", aa) println("&b:", ba) println("&a == &b:", eq) }

results in

&a: 0x5375a0 &b: 0x5375a0 &a == &b: true


Mission accomplished, too complicated to calculate in advance. But globals are bad, so (Go Playground):

func f2() { var ( a struct{} b struct{} aa = &a ba = &b eq = aa == ba ) println("&a:", aa) println("&b:", ba) println("&a == &b:", eq) }

&a: 0xc000046740 &b: 0xc000046740 &a == &b: false


Seems like inlining helps generate false answers.

The interesting part here is that I can create two pointers (which may or may not be equal per specification), but depending on how I compare them I get different results.

Brian Candler

unread,
Jun 19, 2024, 1:17:48 PM (11 days ago) Jun 19
to golang-nuts
There was a similar discussion here: https://groups.google.com/g/golang-nuts/c/JBVqWYFdtC4/m/VJC2OLJcAQAJ

It is very sensitive to exactly how the variables are used. For example, if you replace your println() with fmt.Println() then you get a different answer.

Ian Lance Taylor

unread,
Jun 19, 2024, 1:19:09 PM (11 days ago) Jun 19
to Oliver Eikemeier, golang-nuts
On Wed, Jun 19, 2024 at 9:59 AM Oliver Eikemeier
<eike...@fillmore-labs.com> wrote:
>
> I'm observing some strange behavior and could use some help understanding it.
>
> The specification says: “Pointers to distinct zero-size variables may or may not be equal.”
>
> With the following program (Go Playground):
>
> var ( a struct{} b struct{} eq = &a == &b ) func f1() { println("&a:", &a) println("&b:", &b) println("&a == &b:", eq) }
>
>
> I'll get
>
> &a: 0x537580 &b: 0x537580 &a == &b: false
>
>
> Okay, a and b are empty structs, do not escape, so they share the same address - fine. Also, some optimizer sees that a and b are different variables, so their addresses must be different, and decides to make &a == &b a constant - wrong, but I can live with that.
>
> My question would be: Is this behavior expected, somehow defined by the specification, or is it undefined behavior?

The specs says, as you say above, "Pointers to distinct zero-size
variables may or may not be equal.” That means that you can't predict
the result of any given comparison of addresses of zero-sized
variables. Could be true, could be false, could change each time you
do the comparison. So this behavior is permitted by the spec.



> Let's try to confuse the optimizer a little (Go Playground):
>
> var ( a struct{} b struct{} aa = &a ba = &b eq = aa == ba ) func f1() { println("&a:", aa) println("&b:", ba) println("&a == &b:", eq) }
>
> results in
>
> &a: 0x5375a0 &b: 0x5375a0 &a == &b: true
>
>
> Mission accomplished, too complicated to calculate in advance. But globals are bad, so (Go Playground):
>
> func f2() { var ( a struct{} b struct{} aa = &a ba = &b eq = aa == ba ) println("&a:", aa) println("&b:", ba) println("&a == &b:", eq) }
>
> &a: 0xc000046740 &b: 0xc000046740 &a == &b: false
>
>
> Seems like inlining helps generate false answers.
>
> The interesting part here is that I can create two pointers (which may or may not be equal per specification), but depending on how I compare them I get different results.

Yes, as the spec permits.

Ian

Oliver Eikemeier

unread,
Jun 19, 2024, 4:43:54 PM (10 days ago) Jun 19
to golang-nuts
Hi,

Brian Candler schrieb am Mittwoch, 19. Juni 2024 um 19:17:48 UTC+2:
There was a similar discussion here: https://groups.google.com/g/golang-nuts/c/JBVqWYFdtC4/m/VJC2OLJcAQAJ

Thanks for the link, very insightful.

I learned:

  1. This gets reported about once a year
  2. Pointers do not need to have constant values, i.e. due to a moving GC 
  3. "Pointers to distinct zero-size variables may or may not be equal and the results may or may not be repeatable in any context."
I would considers this as at least worth a FAQ entry. Thanks for the help.

Oliver Eikemeier

unread,
Jun 19, 2024, 4:43:58 PM (10 days ago) Jun 19
to golang-nuts

Hi,

Ian Lance Taylor schrieb am Mittwoch, 19. Juni 2024 um 19:19:09 UTC+2:
On Wed, Jun 19, 2024 at 9:59 AM Oliver Eikemeier <eike...@fillmore-labs.com> wrote:
> The specification says: “Pointers to distinct zero-size variables may or may not be equal.”

The specs says, as you say above, "Pointers to distinct zero-size
variables may or may not be equal.” That means that you can't predict
the result of any given comparison of addresses of zero-sized
variables.

I'm not sure I read it that way. I would interpret it as “taking pointer to distinct zero-size variables may or may not result in equal pointers”.

But not being able to have a repeatable result of a pointer comparison seems strange to me. The specification says “you'll get some pointers”. Fine.

Then I have two pointers. Where in the spec is “the result of comparison of pointers may change over time”?
 
For example (Go Playground):

func f3() {
var (
a  struct{}
b  int
aa = unsafe.Pointer(&a)
ba = unsafe.Pointer(&b)

eq = aa == ba
)

println("&a:", aa)
println("&b:", ba)
println("&a == &b:", eq)
}

gives

&a: 0xc000046738
&b: 0xc000046738
&a == &b: false

and &b is not even a pointer to a zero-sized variable.

Could be true, could be false, could change each time you
do the comparison. So this behavior is permitted by the spec.

I'm not sure about the “undefined behavior over time”. I get that the compiler lays out the memory however it sees fit, but then I should have unchanging values in my variables.
 
> The interesting part here is that I can create two pointers (which may or may not be equal per specification), but depending on how I compare them I get different results.

Yes, as the spec permits. 

 I assume this has little relevance in practice, but it is surprising.

Axel Wagner

unread,
Jun 19, 2024, 5:17:18 PM (10 days ago) Jun 19
to Oliver Eikemeier, golang-nuts
On Wed, 19 Jun 2024 at 22:43, Oliver Eikemeier <eike...@gmail.com> wrote:
The specs says, as you say above, "Pointers to distinct zero-size
variables may or may not be equal.” That means that you can't predict
the result of any given comparison of addresses of zero-sized
variables.

I'm not sure I read it that way. I would interpret it as “taking pointer to distinct zero-size variables may or may not result in equal pointers”.

But not being able to have a repeatable result of a pointer comparison seems strange to me. The specification says “you'll get some pointers”. Fine.

The spec says both. It says
Two distinct zero-size variables may have the same address in memory.
And it says
Pointers to distinct zero-size variables may or may not be equal.

The former means what you say. The latter means what Ian says.

Then I have two pointers. Where in the spec is “the result of comparison of pointers may change over time”?
 
For example (Go Playground):

func f3() {
var (
a  struct{}
b  int
aa = unsafe.Pointer(&a)
ba = unsafe.Pointer(&b)
eq = aa == ba
)

println("&a:", aa)
println("&b:", ba)
println("&a == &b:", eq)
}

gives

&a: 0xc000046738
&b: 0xc000046738
&a == &b: false

and &b is not even a pointer to a zero-sized variable.

Could be true, could be false, could change each time you
do the comparison. So this behavior is permitted by the spec.

I'm not sure about the “undefined behavior over time”. I get that the compiler lays out the memory however it sees fit, but then I should have unchanging values in my variables.
 
> The interesting part here is that I can create two pointers (which may or may not be equal per specification), but depending on how I compare them I get different results.

Yes, as the spec permits. 

 I assume this has little relevance in practice, but it is surprising.

--
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/64e58302-59a6-4cbf-859e-aa6b8a2b6068n%40googlegroups.com.

Oliver Eikemeier

unread,
Jun 20, 2024, 6:29:15 AM (10 days ago) Jun 20
to golang-nuts, Axel Wagner
Let me start with this: I’m fine with the behavior, it is exotic enough that even the optimizer should be allowed to assume something that doesn’t hold during runtime, since it simplifies things.

What I think is at least surprising and at worst lacking is the documentation.

It mentions “pointers” (plural) to zero-sized variables, but for this behavior it is sufficient when only one pointer derives from a pointer to a zero-sized variable, as demonstrated in the example below.

I’m advocating for at least a FAQ article, but also think the specification should be adapted, for clarity but also for the fact that only one pointer pointing to a zero-sized variable can compare differently to anything over time, even things having the same address value.

I do not believe the specification is clear on this. Otherwise I don’t think this is urgent. But it seems to pop up repeatedly.

Cheers
Oliver

Jan Mercl

unread,
Jun 20, 2024, 6:39:15 AM (10 days ago) Jun 20
to Oliver Eikemeier, golang-nuts
On Thu, Jun 20, 2024 at 12:28 PM Oliver Eikemeier
<eike...@fillmore-labs.com> wrote:

> It mentions “pointers” (plural) to zero-sized variables, but for this behavior it is sufficient when only one pointer derives from a pointer to a zero-sized variable, as demonstrated in the example below.

The plural is there because the specification discusses all such
pointers, not a particular case.

The wording is IMO correct and clear. All/any pointers (plural) to
zero sized objects may produce any/unpredictable result in comparison
operations. Relying on the outcome is a bug, because the specs
explicitly say otherwise.

Oliver Eikemeier

unread,
Jun 20, 2024, 7:17:05 AM (10 days ago) Jun 20
to golang-nuts, Jan Mercl

• 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.

Another example (Go Playground):

func f4() {
var (
a  struct{}
b  int
aa = (*int)(unsafe.Pointer(&a))
ba = (*int)(unsafe.Pointer(&b))
eq = aa == ba
)

println("&a:", aa, reflect.TypeOf(aa).String())
println("&b:", ba, reflect.TypeOf(ba).String())
println("&a == &b:", eq)
}

&a: 0xc0000466f8 *int
&b: 0xc0000466f8 *int
&a == &b: false

We do not have “pointers to distinct zero-size variables”, only one pointer derived from a pointer to a zero-sized variable.

I don’t want to be stubborn. What would be the issue with at least adding a FAQ entry? I could open a PR suggesting something, although I’m not a native speaker and might not exactly hit the tone of the document.

Axel Wagner

unread,
Jun 20, 2024, 7:24:58 AM (10 days ago) Jun 20
to Oliver Eikemeier, golang-nuts
On Thu, 20 Jun 2024 at 12:28, Oliver Eikemeier <eike...@fillmore-labs.com> wrote:
Let me start with this: I’m fine with the behavior, it is exotic enough that even the optimizer should be allowed to assume something that doesn’t hold during runtime, since it simplifies things.

What I think is at least surprising and at worst lacking is the documentation.

It mentions “pointers” (plural) to zero-sized variables, but for this behavior it is sufficient when only one pointer derives from a pointer to a zero-sized variable, as demonstrated in the example below.

Re-visiting your example, it is indeed interesting that I realized before. I'll note that you need to involve `unsafe.Pointer` to observe this, as you may not compare pointers to different types. And `unsafe.Pointer`'s behaviors are somewhat left up to the implementation.


A Pointer is a pointer type but a Pointer value may not be dereferenced. Any pointer or value of core type uintptr can be converted to a type of core type Pointer and vice versa. The effect of converting between Pointer and uintptr is implementation-defined.

The thing that stands out is that it leaves the effect of converting to `uintptr` up to the implementation, but nothing else. So any other rule, as far as the language is concerned, should be derived from the rules for pointer types (except that they can not be dereferenced).

One contradiction here is that this section references the definition of a pointer type, which says

A pointer type denotes the set of all pointers to variables of a given type, called the base type of the pointer.

This contradicts the nature of `unsafe.Pointer`, though, which does not have a base type. That difference is what causes the trouble: When the spec defines pointer comparisons by

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.
 
We can see that 1. `unsafe.Pointer` is definitionally a pointer type, 2. in your example, the two `unsafe.Pointers` do not point to the same variable and do not have the value `nil` and 3. are not pointing at distinct zero-sized variables. So their comparison should be `false`, which is what your example observes.

All of this would be fine. What makes your example confusing is that you use `println` to output them and see that they "have the same value". For that, we need to look at the definition of `println`:

Current implementations provide several built-in functions useful during bootstrapping. These functions are documented for completeness but are not guaranteed to stay in the language. They do not return a result.

Function   Behavior

print      prints all arguments; formatting of arguments is implementation-specific
println    like print but prints spaces between arguments and a newline at the end

Implementation restriction: print and println need not accept arbitrary argument types, but printing of boolean, numeric, and string types must be supported.

Notably, this says nothing about what happens if you pass a pointer type. That is thus left up to the implementation. In effect, it does the same as `fmt.Println` ultimately: It converts the pointer to `uintptr`, which as we've seen above is left to the implementation.

Note that if you *don't* convert an `unsafe.Pointer` to `uintptr`, you have no way to argue that they "are actually equal". And if you don't use `unsafe.Pointer` you have no way to compare pointers to variables of different types. So, the only way to observe that behavior is to enter what is implementation-defined.

So, yes. Your example is interesting, but still within spec.

I’m advocating for at least a FAQ article,

I tend to agree, though I'm not sure how to phrase that, beyond saying "do not make any assumptions about the identity pointers to zero-sized variables or with zero-sized base types or that where derived from one of those" and I'm not sure how helpful that is.
 
but also think the specification should be adapted, for clarity but also for the fact that only one pointer pointing to a zero-sized variable can compare differently to anything over time, even things having the same address value.

Note that "address value" is not something that exists within the spec. As for clarifying the spec here, maybe. I do think the behavior is covered, as I outlined above. And as above, I'm not sure how to clarify it further, while still leaving up the space we want to left open.

Jan Mercl

unread,
Jun 20, 2024, 7:25:57 AM (10 days ago) Jun 20
to Oliver Eikemeier, golang-nuts
On Thu, Jun 20, 2024 at 1:16 PM Oliver Eikemeier
<eike...@fillmore-labs.com> wrote:

> • 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.

Compare to (made up) "NaNs always compare non-equal". It talks both
about '3.14 == NaN' and 'NaN == NaN'. The plural in our context means
"any, all and every". It does not specify only the last case of two
NaNs.

My 2c.

Axel Wagner

unread,
Jun 20, 2024, 7:26:31 AM (10 days ago) Jun 20
to Oliver Eikemeier, golang-nuts
(apologies for the abundance of grammatical and spelling errors that occurred during editing and of course only became visible after hitting "send")

Jan Mercl

unread,
Jun 20, 2024, 7:29:10 AM (10 days ago) Jun 20
to Axel Wagner, Oliver Eikemeier, golang-nuts
On Thu, Jun 20, 2024 at 1:26 PM 'Axel Wagner' via golang-nuts
<golan...@googlegroups.com> wrote:

> (apologies for the abundance of grammatical and spelling errors that occurred during editing and of course only became visible after hitting "send")

(Send collapses the wave function ;-)

Oliver Eikemeier

unread,
Jun 20, 2024, 7:49:42 AM (10 days ago) Jun 20
to golang-nuts, Axel Wagner
Am 20.06.2024 um 13:24 schrieb Axel Wagner <axel.wa...@googlemail.com>:

We can see that 1. `unsafe.Pointer` is definitionally a pointer type, 2. in your example, the two `unsafe.Pointers` do not point to the same variable and do not have the value `nil` and 3. are not pointing at distinct zero-sized variables. So their comparison should be `false`, which is what your example observes.

Does it (Go Playground)?

func f5() {
var (
a  struct{}
b  int
aa = (*int)(unsafe.Pointer(&a))
ba = (*int)(unsafe.Pointer(&b))
eq = aa == ba
)

println("&a:", aa, reflect.TypeOf(aa).String())
println("&b:", ba, reflect.TypeOf(ba).String())
println("&a == &b:", eq)
cmpIntPtr(aa, ba)
}

func cmpIntPtr(p1 *int, p2 *int) {
fmt.Println("p1 == p2:", p1 == p2)
}

&a: 0xc0000466f8 *int
&b: 0xc0000466f8 *int
&a == &b: false
p1 == p2: true


All of this would be fine. What makes your example confusing is that you use `println` to output them and see that they "have the same value".

Not really. I use println to not let them escape (which would change results, obviously) and only print out the pointer values to show that we can construct some piece of code that sees them as identical, see above.

Note that if you *don't* convert an `unsafe.Pointer` to `uintptr`, you have no way to argue that they "are actually equal"

I can by doing something like

*aa = 42
fmt.Println(aa == ba, *ba)

Two pointer values are equal if they point to the same variable - aa and ba do. Note that “A variable is a storage location for holding a value.

I’m advocating for at least a FAQ article,

I tend to agree, though I'm not sure how to phrase that, beyond saying "do not make any assumptions about the identity pointers to zero-sized variables or with zero-sized base types or that where derived from one of those" and I'm not sure how helpful that is.

It seems like a frequently asked question to me ;)

but also think the specification should be adapted, for clarity but also for the fact that only one pointer pointing to a zero-sized variable can compare differently to anything over time, even things having the same address value.

Note that "address value" is not something that exists within the spec. As for clarifying the spec here, maybe. I do think the behavior is covered, as I outlined above. And as above, I'm not sure how to clarify it further, while still leaving up the space we want to left open.

I vote for making this openness explicit, meaning that even the optimizer is free to make assumptions that do not hold during runtime. So if you have a pointer derived from something pointing to a zero-size type it can compare with different results over time, even false when having the same value. I do not believe that is what’s currently in the spec.

Cheers
Oliver

Axel Wagner

unread,
Jun 20, 2024, 8:13:07 AM (10 days ago) Jun 20
to Oliver Eikemeier, golang-nuts
On Thu, 20 Jun 2024 at 13:48, Oliver Eikemeier <eike...@fillmore-labs.com> wrote:

Am 20.06.2024 um 13:24 schrieb Axel Wagner <axel.wa...@googlemail.com>:

We can see that 1. `unsafe.Pointer` is definitionally a pointer type, 2. in your example, the two `unsafe.Pointers` do not point to the same variable and do not have the value `nil` and 3. are not pointing at distinct zero-sized variables. So their comparison should be `false`, which is what your example observes.

Does it (Go Playground)?

func f5() {
var (
a  struct{}
b  int
aa = (*int)(unsafe.Pointer(&a))
ba = (*int)(unsafe.Pointer(&b))
eq = aa == ba
)

println("&a:", aa, reflect.TypeOf(aa).String())
println("&b:", ba, reflect.TypeOf(ba).String())
println("&a == &b:", eq)
cmpIntPtr(aa, ba)
}

func cmpIntPtr(p1 *int, p2 *int) {
fmt.Println("p1 == p2:", p1 == p2)
}

&a: 0xc0000466f8 *int
&b: 0xc0000466f8 *int
&a == &b: false
p1 == p2: true

Interestingly, the spec does not forbid that (or specifies it as implementation-defined), though the (implementation-defined) unsafe.Pointer rules do, as using `unsafe.Pointer` to convert between pointers of different base types is only allowed if the target variable is at least as large as the source variable and both share an equivalent memory layout.

I think the fact that the spec does not put any limits on the allowed behaviour of converting between `unsafe.Pointer` and other pointer types is a clear oversight that should be corrected (perhaps it should really be "The effect of converting between Pointer and uintptr is implementation-defined" should really say "and other types"). But I don't think it's a surprising fact that you can make a program behave in almost arbitrary ways by doing that.
 
I’m advocating for at least a FAQ article,

I tend to agree, though I'm not sure how to phrase that, beyond saying "do not make any assumptions about the identity pointers to zero-sized variables or with zero-sized base types or that where derived from one of those" and I'm not sure how helpful that is.

It seems like a frequently asked question to me ;)

It is. But note that every time it is asked, it leads to considerable discussion, which seems to strongly suggest that it is hard to give a simple, unambiguous answer that people find satisfying.

I think you should acknowledge that the spec *is* already trying to be very clear about this. The fact that you can poke holes in it is because writing a spec unambiguously is hard. But the two quoted phrases are there in the best attempt to clearly state that you can not rely on any assumptions about pointers to zero-sized variables - within the somewhat abstract framework of the spec. Feel free to propose a better one, I'm not actually arguing against that.

And I'm also not arguing against an FAQ entry. Ideally, someone would suggest a phrasing that is sufficiently broad and unambiguous and clear to cover any version of this question people might ask, while also not contradicting the spec.

Robert Engels

unread,
Jun 20, 2024, 8:22:01 AM (10 days ago) Jun 20
to Axel Wagner, Oliver Eikemeier, golang-nuts
I would add that I think the Go team should strive to remove as much “implementation defined” or “undefined” from the spec as possible. Forgo the optimizations. It’s this language that makes C++ such a pain to work with especially in a cross platform manner. 

I think you can safely narrow the spec in this area without issue - since it would have been very difficult to use anyway. Maybe some highly specialized code that relied on a particular Go implementation might. 

On Jun 20, 2024, at 7:12 AM, 'Axel Wagner' via golang-nuts <golan...@googlegroups.com> wrote:


--
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.

Axel Wagner

unread,
Jun 20, 2024, 8:31:01 AM (10 days ago) Jun 20
to Oliver Eikemeier, golang-nuts
FYI, filed https://github.com/golang/go/issues/68086 for the spec-hole that conversions between unsafe.Pointer and other pointer types are not implementation defined.

Axel Wagner

unread,
Jun 20, 2024, 8:40:23 AM (10 days ago) Jun 20
to Robert Engels, Oliver Eikemeier, golang-nuts
On Thu, 20 Jun 2024 at 14:21, Robert Engels <ren...@ix.netcom.com> wrote:
I would add that I think the Go team should strive to remove as much “implementation defined” or “undefined” from the spec as possible. Forgo the optimizations. It’s this language that makes C++ such a pain to work with especially in a cross platform manner. 

I think you can safely narrow the spec in this area without issue - since it would have been very difficult to use anyway. Maybe some highly specialized code that relied on a particular Go implementation might. 

I strongly disagree. There is a lot of software out there that relies on zero-sized fields to achieve certain semantic behaviours (like preventing copying or using unkeyed struct literals) or alignments. They all assume that the size of a struct is not impacted by that. Without this "optimization" that would no longer be true and every zero-sized field must take at least one byte of memory, to be able to guarantee that they have different addresses. That is just not going to happen.

On the other hand, removing the restriction in the other direction and just always making it false is what I suggested last time and there are reasons in that issue, why that isn't going to happen either.

Robert Engels

unread,
Jun 20, 2024, 8:47:48 AM (10 days ago) Jun 20
to Axel Wagner, Oliver Eikemeier, golang-nuts
What I asked did not require the first, I was referring more about the latter. So regardless of how it is implemented it is always false. 

On Jun 20, 2024, at 7:39 AM, Axel Wagner <axel.wa...@googlemail.com> wrote:



Oliver Eikemeier

unread,
Jun 20, 2024, 9:49:55 AM (10 days ago) Jun 20
to golang-nuts, Axel Wagner, Robert Engels


Am 20.06.2024 um 14:12 schrieb Axel Wagner <axel.wa...@googlemail.com>:

On Thu, 20 Jun 2024 at 13:48, Oliver Eikemeier <eike...@fillmore-labs.com> wrote:

I think you should acknowledge that the spec *is* already trying to be very clear about this. The fact that you can poke holes in it is because writing a spec unambiguously is hard.

I acknowledge that - that’s why I care in the first place and also - as mentioned - try not to be stubborn. Although the latter is not always easy in internet discussions, but I digress.

But the two quoted phrases are there in the best attempt to clearly state that you can not rely on any assumptions about pointers to zero-sized variables - within the somewhat abstract framework of the spec. Feel free to propose a better one, I'm not actually arguing against that.

Yeah, I’m still trying to understand the issue, the discussion helps, so thanks for that.

Am 20.06.2024 um 14:21 schrieb Robert Engels <ren...@ix.netcom.com>:

I would add that I think the Go team should strive to remove as much “implementation defined” or “undefined” from the spec as possible. Forgo the optimizations. It’s this language that makes C++ such a pain to work with especially in a cross platform manner.

I think you can safely narrow the spec in this area without issue - since it would have been very difficult to use anyway. Maybe some highly specialized code that relied on a particular Go implementation might. 

What happens seems to be:

a) In practice the runtime just uses pointers as memory addresses, so everything is fine there.

b) Pointers (especially unsafe pointers) being address in memory (not only “a storage location for holding a value”) is not really specified, but sometimes useful. The standard runtime uses casting of unsafe pointer for multiple reasons.

c) Pointers to zero sized variables have to point somewhere. Since the variable has zero size, some aliasing is easily possible.

d) The optimizer is free to make assumptions about aliasing that sometimes don’t match the memory alignment during runtime, disagreeing with a).

I’m not advocating against the optimizer making assumptions about aliasing that are not true, it seems useful to me.

On the other hand you can do surprising constructs (Go Playground):

func f7() {
var (
a  struct{}
b  int
aa = (*int)(unsafe.Pointer(&a))
ba = (*int)(unsafe.Pointer(&b))
)

println("&a:", aa, reflect.TypeOf(aa).String())
println("&b:", ba, reflect.TypeOf(ba).String())

*ba = 0

*aa++
println(*ba, aa == ba)

*ba++
println(*aa, ba == aa)
}

&a: 0xc0000466f8 *int
&b: 0xc0000466f8 *int
0 false
2 false

There are definitely dragons. I might need some more time to think over this, but thanks to every participant for the useful input.

Cheers
Oliver

Axel Wagner

unread,
Jun 20, 2024, 9:58:28 AM (10 days ago) Jun 20
to Oliver Eikemeier, golang-nuts, Robert Engels
I don't believe the issue in this case really is to do with zero-sized variables. Here is an example with the same output, not involving any zero-sized variables:
The issue is that you treat a `*struct{}` as an `*int` - that is, you explicitly obtain a pointer to invalid memory - and then expect that to have any reasonable behaviour.

Oliver Eikemeier

unread,
Jun 20, 2024, 10:19:20 AM (10 days ago) Jun 20
to golang-nuts, Axel Wagner
Thanks for that example.

The issue is that you and I have constructed aliased variables that the optimizer assumes are not aliased. Therefore the “false” on comparison, which also happens with pointers to zero-sized variables, but is much more common in the latter case.

This is interesting. I’m not enough of a compiler buff to say that this assumption is reasonable - Go is 14 years old, so it seems to work - but obviously one can write code where it is false, the “zero-sized pointers” being the most prominent example. Anyone has a pointer to the alias analysis design of the Go compiler?

Axel Wagner

unread,
Jun 20, 2024, 10:25:31 AM (10 days ago) Jun 20
to Oliver Eikemeier, golang-nuts
I want to re-emphasise that both of these examples violate the allowed usages of unsafe.Pointer defined by gc. So I would argue that, yes, its expectation is reasonable, because it explicitly told you not to create this situation.

If you can construct a program that does not violate the unsafe.Pointer rules and yet behaves unexpectedly, the question might be more ambiguous.

tapi...@gmail.com

unread,
Jun 20, 2024, 1:32:21 PM (10 days ago) Jun 20
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
}
}

Axel Wagner

unread,
Jun 26, 2024, 12:37:40 AM (4 days ago) Jun 26
to Oliver Eikemeier, golang-nuts
Hi,

you might be interested to learn that Ian has filed a CL adding an FAQ entry.

Ian Lance Taylor

unread,
Jun 26, 2024, 11:17:46 PM (3 days ago) Jun 26
to Axel Wagner, Oliver Eikemeier, golang-nuts
On Tue, Jun 25, 2024 at 9:37 PM 'Axel Wagner' via golang-nuts
<golan...@googlegroups.com> wrote:
>
> you might be interested to learn that Ian has filed a CL adding an FAQ entry.

Now committed at https://go.dev/doc/faq#zero_size_types .

Ian
Reply all
Reply to author
Forward
0 new messages