
Type-based aliasing in C - Kristine1975
http://kristerw.blogspot.com/2016/05/type-based-aliasing-in-c.html
======
kazinator
The float/int aliasing via pointer is the strawman version of this which is
used to sell the undefinedness and the optimizations as a good thing.

In fact, int/ _int_ aliasing is also undefined behavior:

    
    
       {
         struct foo { int x; } sfoo = { 42; };
         struct bar { int x; } *psbar = (struct bar *) &sfoo;
    
         return psbar->x; // UB, even though int accessed.
       }
    

Why this is undefined ias that psbar->x is actually (* psbar).x. There is an
lvalue ere which is (* psbar) and that is of type struct bar; it doesn't match
the type of the object which is struct foo.

Maybe it's time for an alternative to ISO C: a safe C with more behaviors
pinned down.

It should be that when an lvalue is formed by a chained expression like
a->b[2].c.d the type of the access should be considered to be the final type
which designates the data object, without regard for the intermediate types in
the chain. The object should be deemed to be accessed via that type. So if the
.d member in a->b[2].c.d has type int, and the object being designated has
that type, everything is good.

~~~
gpderetta
Why would you expect the above to work? It is a pretty clear violation of the
type rules on any sane language.

For what is worth, I would love to be able to wrap logically different types
of float and ints in distinct structures so that I can benefit from TBAA more,
but I believe that GCC, for many reasons, internally uses a structural type
system to track TBAA.

~~~
kazinator
> _It is a pretty clear violation of the type rules on any sane language._

Is it? What is "sane"?

    
    
      This is the TXR Lisp interactive listener of TXR 141.
      Use the :quit command or type Ctrl-D on empty line to exit.
      1> (defstruct foo nil x)
      #<struct-type foo>
      2> (defstruct bar nil x)
      #<struct-type bar>
      3> (defun access-x (obj) obj.x)
      access-x
      4> (access-x (new foo x 42))
      42
      5> (access-x (new bar x 42))
      42
    

This is so insane that it will work even if x is at different offsets in foo
and bar.

We can whip up a similar thing statically with, say, C++ templates.

Note that C _does_ explicitly allow this:

    
    
       {
         struct foo { int x; char other; };
         struct bar { int x; double other; };
         union combined { struct foo f; struct bar b; } c;
    
         c.f.x = 42;
         return c.b.x;
       }
    

If structs which have some common sequence of leading members (same names and
types) are combined via a union, then those common members can be accessed
through any member of the that union.

~~~
lmm
> This is so insane that it will work even if x is at different offsets in foo
> and bar.

You're accessing foo.x or bar.x. You're just doing it in a generic way. That's
fine and should work, in any language.

> If structs which have some common sequence of leading members (same names
> and types) are combined via a union, then those common members can be
> accessed through any member of the that union.

Unions have defined semantics in all cases, no?

    
    
        union combined { float f; int i; } c;
        c.f = 1.0f
        return c.i;
    

will return some (possibly implementation-defined) value rather than being
undefined behaviour, because that's the whole point of a union - access to
different fields of a union shouldn't be an aliasing violation.

~~~
kazinator
Unions have well-defined semantics in that special case, spelled out in ISO C.

I have a copy of C99 handy, where it is paragraph 5 in 6.5.2.3 (Structure and
union members):

 _" One special guarantee is made in order to simplify the use of unions: if a
union contains several structures that share a common initial sequence (see
below), and if the union object currently contains one of these structures, it
is permitted to inspect the common initial part of any of them anywhere that a
declaration of the complete type of the union is visible. Two structures share
a common initial sequence if corresponding members have compatible types (and,
for bit-fields, the same widths) for a sequence of one or more initial
members."_

The part about "anywhere that a declaration of the complete type of the union
is visible" isn't superfluous verbiage: it means that the aliasing doesn't
have to go through the union; but it has to be in a region of code where the
union is declared. So for instance, it is no longer well-defined if you do
this:

    
    
       // external fun has no idea about the union
       external_fun(&c.f);
    
       // exernal_fun is this:
    
       void external_fun(struct foo *pf)
       {
         struct bar *pb = (struct bar *) pf;
         // access through pb
       }
    

Here, c.f is a member of a union where and has a "common initial part" with
c.b of the same union object. But this is not known in external_fun which has
no declaration of the union and so the translation of external_fun doesn't
have to honor this aliasing if there is no completely declared union type in
scope which aliases struct foo and struct bar.

(See, C goes out of its way to make it clear that aliasing between different
struct types is bad: even when an exception is made, there is weasel wording
to carefully contain it!)

~~~
gpderetta
The 'visible union' rule is generally understood as meaning that the union
type must be part of the access expression. Any other interpretation that only
requires the union to be in scope would pretty much render TBAA moot for any
non trivial program.

And yes, this rule is part of the reason the current TBAA rules are such a
mess. There is an outstanding C issue to clarify the wording here but the C
commitee is severely under capacity.

There are proposed C++ wordings that clarify this rule in addition to
generally clarify lifetimes and TBAA (while still being far from settling the
issue, mind you). These wordings also introduce std::launder(), a function
that bless some of the type punning tricks by explicitly informing the
compiler about what's going on.

~~~
kazinator
> _Any other interpretation that only requires the union to be in scope would
> pretty much render TBAA moot for any non trivial program._

An interpretation of the 'visible union' rule as being in scope is the only
interpretation which doesn't render that text completely unnecessary.

Of course if the access is taking place through the union, the union must be
completely declared! When would you be able to access a union member as u.x,
or pu->x, when the type of u or *pu is incomplete?

------
_yosefk
I didn't know the part where the standard lets you malloc a buffer, write it
as int, read it as int, then write it as float, and read it as float, all
without freeing, and how the compiler thus has to treat heap memory
differently from other memory. The part where compilers are too aggressive to
follow this rule actually makes sense (as it seems to greatly increase the
chance of _valid_ optimizations based on type-based alias analysis to actually
happen; it's hard to prove that a given pointer wasn't malloced.)

I wonder if the story is different with C++'s operator new, which returns a
typed result.

~~~
gpderetta
Yes, the C rules are weird as that. I believe that GCC semi official stance is
that stores always change the effective type of memory, not just for anonymous
memory (this is more relaxed than the C rules, but handles correctly cases
which are ambiguous under the standard rules).

C++ has the same rules as C for trivially constructible objects, but for
objects with non trivial constructors, things are more complicated (and keep
being revised at every standard update).

------
haberman
There is a big problem AFAIK with the "effective type" (C99) or "dynamic type"
(C++) rules. If you are writing your own allocator (malloc() or some arena
allocator), there is no legal way AFAIK to seed it with global/stack memory.
Your global/stack memory has a declared type of char[], and there is no way to
convert this into the type-less kind of memory you need to get back from
malloc().

------
ekr
One of the way to achieve encapsulation in C, is to have a certain definition
of a structure in the implementation file, that you use internally, and a
different (usually smaller, sometimes no structure at all, just a handle),
exposed to the public (in the header).

As far as I can see, this would break this approach, (if this strict type
aliasing would also be used at LTO).

~~~
gpderetta
In C++ this already breaks the One Definition Rule at compile time, even
before any aliasing issues come into play. I'm not sure what's the ODR
equivalent in C (or whether it exists at all).

