2

Given the following code:

struct Tag {};
struct X {
//    Tag t; // if not commented it shouldn't be pointer-interconvertible
    int k;
};

int fn(const X& x, int& p) {
    int i = x.k;
    p = 2;
    return i + x.k;
}

The generated code is:

fn(X const&, int&):
        mov     eax, DWORD PTR [rdi]
        mov     DWORD PTR [rsi], 2
        add     eax, DWORD PTR [rdi]
        ret

Here the compiler assumes aliasing.

If member t is not present, the types X and int are pointer-interconvertible. As so, the compiler must generate code as if the references could alias.

But if member t is present, they should no longer be pointer-interconvertible and code for the non-aliased case should be generated. But in both cases the code is identical except the relative address of member k.

The assembler:

fn(X const&, int&):
        mov     eax, DWORD PTR [rdi+4]
        mov     DWORD PTR [rsi], 2
        add     eax, DWORD PTR [rdi+4]
        ret

As an counter-example

template<typename T>
struct X {int k; };

int fn(X<struct A>& x, X<struct B>& p) {
    int i = x.k;
    p.k = 2;
    return i + x.k;
}

in the above version the generated code assumes no aliasing, but the types are pointer-interconvertible.

fn(X<A>&, X<B>&):
        mov     eax, DWORD PTR [rdi]
        mov     DWORD PTR [rsi], 2
        add     eax, eax
        ret

Can anyone explain this?

wimalopaan
  • 4,838
  • 1
  • 21
  • 39

2 Answers2

4

Here

int fn(const X& x, int& p) {
    int i = x.k;
    p = 2;
    return i + x.k;
}

the compiler has to assume that p may be a reference to x.k. Both p and x.k are lvalues of type int. Thus, they may be aliasing each other. Whether X is pointer-interconvertible with an int or not does not change the fact that p may be a reference to x.k.

Here

int fn(X<struct A>& x, X<struct B>& p) {
    int i = x.k;
    p.k = 2;
    return i + x.k;
}

on the other hand, X<struct A> and X<struct B> are completely unrelated types. x and p cannot be references to the same object. Thus, x.k and p.k cannot denote the same subobject. The fact that X<struct A> as well as X<struct B> are both pointer-interconvertible with an int is, again, irrelevant…

Michael Kenzel
  • 15,508
  • 2
  • 30
  • 39
  • i realized I wrote almost exact same answer, just not as nice as yours ;) – 463035818_is_not_an_ai Jan 28 '20 at 20:43
  • @formerlyknownas_463035818 well, thank you. have an upvote anyways… :D – Michael Kenzel Jan 28 '20 at 20:46
  • But what if k is private and X has operator int() const returning k ? See :https://gcc.godbolt.org/z/o3cA86 – wimalopaan Jan 28 '20 at 20:47
  • @wimalopaan what would that change? – Michael Kenzel Jan 28 '20 at 20:51
  • `int& p` can't reference `x.k`. – wimalopaan Jan 28 '20 at 20:52
  • 1
    @wimalopaan well, if the compiler can prove that that's the case, it can, of course, take advantage of that information. In general, a compiler is *allowed* to optimize the generated machine code under the as-if rule. A compiler is not required to perform this optimization, however. Compilers are generally pretty conservative when it comes to taking advantage of strict aliasing because too much code depends on all sorts of technically undefined behavior just doing what the programmer expected anyways rather than keeping to the bare minimum of what behavior the standard actually guarantees… – Michael Kenzel Jan 28 '20 at 21:00
  • Well, what UB-way could you imagine, so that the compilers pessimizes in that way. – wimalopaan Jan 28 '20 at 21:09
  • 1
    @wimalopaan trickery with `offsetof` or member pointers, for example. People like to treat objects as just a bunch of bytes they can do with whatever they want. Some people even like to just `#define private public` in the one or other translation unit every once in a while… – Michael Kenzel Jan 28 '20 at 21:14
  • 1
    I'm not a compiler engineer, but I would imagine that it just gets really hard to prove that two pointers/references do not alias in general. So I would guess that compilers just don't bother to go through the trouble of taking into account accessibility of members in their alias analysis since there is not all that much to gain from that except a huge potential for introducing a lot of bugs of arguably the worst kind. Consider all the possibilities in which some piece of code somewhere outside the current translation unit could somehow end up able to access the member in question again… – Michael Kenzel Jan 28 '20 at 21:14
3

Here

int fn(const X& x, int& p) {
    int i = x.k;
    p = 2;
    return i + x.k;
}

X::k is int,p is a reference to int. p can be a reference to x.k.

On the other hand, here:

int fn(X<struct A>& x, X<struct B>& p) {
    int i = x.k;
    p.k = 2;
    return i + x.k;
}

X<struct A> and X<struct B> are distinct types. There is no way to have x and p or parts of it refer to the same object.

But what if k is private and X has operator int() const returning k ?

Then nothing changes. Sloppy speaking, you need a reference/pointer to get potential aliasing. For example

struct G {};
struct H { G* g; }

void foo(G* a,H b);

Here b.g and a can point to the same G (note that this is the case no matter if b is passed by value, reference or pointer). In your example...

template<typename T>
struct X {int k; };
int fn(X<struct A>& x, X<struct B>& p)

.. the only references are x and p. They refer to objects of different types, aka different objects.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185