Live data from Hacker News

The Strict Aliasing Situation Is Pretty Bad

blog.regehr.org

1–10 of 70 posts

Re: The Strict Aliasing Situation Is Pretty Bad

#3
I thought this article was unnecessarily dire.

One section claims "Physical Subtyping is Broken", where "physical subtyping" is defined as "the struct-based implementation of inheritance in C." I assume this means the typical pattern of:

    typedef struct {
       int base_member_1;
       int base_member_2;
    } Base;

    typedef struct {
       Base base;

       int derived_member 1;
    } Derived;
The article claims physical subtyping is broken because casting between pointer types results in undefined behavior. The article gives this example:

    #include 
     
    typedef struct { int i1; } s1;
    typedef struct { int i2; } s2;
     
    void f(s1 *s1p, s2 *s2p) {
      s1p->i1 = 2;
      s2p->i2 = 3;
      printf("%i\n", s1p->i1);
    }
     
    int main() {
      s1 s = {.i1 = 1};
      f(&s, (s2 *)&s);
    }
I agree this example is broken, but casting between pointer types in this way is totally unnecessary for C-based inheritance. You can do upcasts and downcasts that are totally legal:

    Derived d;

    // Legal upcast:
    Base* base = &d->base;

    // Legal downcast:
    Derived* derived = (Derived*)base;
So I don't think the article has proved that "Physical Subtyping is Broken."

The next section says that "Chunking Optimizations Are Broken," because code like this is illegal:

    void copy_8_bytes(char *dst, const char *src) {
      *(uint64_t*)dst = *(uint64_t*)src;
    }
While this is true, such optimizations are generally unnecessary. For example, write this instead as:

    void copy_8_bytes(char *dst, const char *src) {
      memcpy(dst, src, 8);
    }
If you compile this on an architecture like x86 that truly allows unaligned reads, you'll see that modern compilers do the "chunking optimization" for you:

    0000000000000000 :
       0:   48 8b 06                mov    rax,QWORD PTR [rsi]
       3:   48 89 07                mov    QWORD PTR [rdi],rax
       6:   c3                      ret
It says next that "int8_t and uint8_t Are Not Necessarily Character Types." That is indeed a good point and probably not well-known. So I agree this is something people should keep in mind. But most of this article is warning against practices that are generally unnecessary and known to be bad C in 2016.

It's true that a lot of legacy code-bases still break these rules. But many are cleaning up their act, fixing practices that were never correct but used to work. For example, here is an example of Python fixing its API to comply with strict aliasing, and this is from almost 10 years ago: https://www.python.org/dev/peps/pep-3123/

Re: The Strict Aliasing Situation Is Pretty Bad

#4
post #3

I thought this article was unnecessarily dire. One section claims "Physical Subtyping is Broken", where "physical subtyping" is defined as "the struct-based implementation of inheritance in C." I assume this means the typical pattern of: typedef struct { int base_member_1; int base_member_2; } Base; typedef struct { Base base; int derived_member 1; } Derived; The article claims physical subtyping is broken because ca…

If you do this

    Derived* derived = (Derived*)base;
and then use both base and derived, is that not violating aliasing rules?

Re: The Strict Aliasing Situation Is Pretty Bad

#5
post #3

I thought this article was unnecessarily dire. One section claims "Physical Subtyping is Broken", where "physical subtyping" is defined as "the struct-based implementation of inheritance in C." I assume this means the typical pattern of: typedef struct { int base_member_1; int base_member_2; } Base; typedef struct { Base base; int derived_member 1; } Derived; The article claims physical subtyping is broken because ca…

I understand the upcast (which is certainly legal but it forces the casting code to know the depth of the inheritance hierarchy - as in &derived->base1.base2), but what's the argument making the downcast back to Derived legal C? (I honestly wonder; personally I either compile with -fno-strict-aliasing or trust my tests to validate the build...)

Re: The Strict Aliasing Situation Is Pretty Bad

#6
post #3

I thought this article was unnecessarily dire. One section claims "Physical Subtyping is Broken", where "physical subtyping" is defined as "the struct-based implementation of inheritance in C." I assume this means the typical pattern of: typedef struct { int base_member_1; int base_member_2; } Base; typedef struct { Base base; int derived_member 1; } Derived; The article claims physical subtyping is broken because ca…

Actually, casts to/from char * are always defined in C (chars are always assumed to alias). The author was talking about "chunking" non-char units.

Re: The Strict Aliasing Situation Is Pretty Bad

#7
post #5
post #3

I thought this article was unnecessarily dire. One section claims "Physical Subtyping is Broken", where "physical subtyping" is defined as "the struct-based implementation of inheritance in C." I assume this means the typical pattern of: typedef struct { int base_member_1; int base_member_2; } Base; typedef struct { Base base; int derived_member 1; } Derived; The article claims physical subtyping is broken because ca…

I understand the upcast (which is certainly legal but it forces the casting code to know the depth of the inheritance hierarchy - as in &derived->base1.base2), but what's the argument making the downcast back to Derived legal C? (I honestly wonder; personally I either compile with -fno-strict-aliasing or trust my tests to validate the build...)

> I understand the upcast (which is certainly legal but it forces the casting code to know the depth of the inheritance hierarchy - as in &derived->base1.base2)

If this is inconvenient, just casting directly to Base pointer is also legal.

> but what's the argument making the downcast back to Derived legal C?

The justification comes from this part of the C standard (C99 6.7.2.1 p13):

    Within a structure object, the non-bit-field
    members and the units in which bit-fields reside
    have addresses that increase in the order in which
    they are declared. A pointer to a structure
    object, suitably converted, points to its initial
    member (or if that member is a bit-field, then to
    the unit in which it resides), and vice versa.
    There may be unnamed padding within a structure
    object, but not at its beginning.
It follows that:

    Derived *d = GetDerived();

    // This is legal: a pointer to Derived, suitably converted,
    // points to its initial member "base":
    Base *base = (Base*)d;

    // This is also legal: a pointer to Derived.base, the initial
    // member of Derived, suitably converted, points to Derived.
    Derived *d2 = (Derived*)base;

Re: The Strict Aliasing Situation Is Pretty Bad

#8
post #3

I thought this article was unnecessarily dire. One section claims "Physical Subtyping is Broken", where "physical subtyping" is defined as "the struct-based implementation of inheritance in C." I assume this means the typical pattern of: typedef struct { int base_member_1; int base_member_2; } Base; typedef struct { Base base; int derived_member 1; } Derived; The article claims physical subtyping is broken because ca…

Actually, casts to/from char * are always defined in C (chars are always assumed to alias). The author was talking about "chunking" non-char units.

> Actually, casts to/from char * are always defined in C (chars are always assumed to alias).

Not true. The standard says you can access any object's value via the char type, but not the reverse. You can't cast a character array to any type and dereference it.

> The author was talking about "chunking" non-char units.

Sure, but you can call my copy_8_bytes() function like so legally:

    int64_t a, b = 0;
    copy_8_bytes(&a, &b);
So what I said applies to non-char types.

Re: The Strict Aliasing Situation Is Pretty Bad

#9
post #3

I thought this article was unnecessarily dire. One section claims "Physical Subtyping is Broken", where "physical subtyping" is defined as "the struct-based implementation of inheritance in C." I assume this means the typical pattern of: typedef struct { int base_member_1; int base_member_2; } Base; typedef struct { Base base; int derived_member 1; } Derived; The article claims physical subtyping is broken because ca…

If you do this Derived* derived = (Derived*)base; and then use both base and derived, is that not violating aliasing rules?

Pretty sure it's safe! Take this program:

    typedef struct {
      int x;
    } Base;

    typedef struct {
      Base base;
      int y;
    } Derived;

    int f(Base* b, Derived* d) {
      b->x = 0;
      d->base.x = 1;
      return b->x;
    }
Notice that if we are accessing the base members of "d", we are still accessing them through a struct of type "Base" (d->base.x). If we compile this with strict aliasing, you can see the output is allowing that the two might alias (while this isn't a proof, it's a strong indication that this is aliasing-correct).

    0000000000000000 :
       0:   c7 07 00 00 00 00       mov    DWORD PTR [rdi],0x0
       6:   c7 06 01 00 00 00       mov    DWORD PTR [rsi],0x1
       c:   8b 07                   mov    eax,DWORD PTR [rdi]
       e:   c3                      ret

Re: The Strict Aliasing Situation Is Pretty Bad

#10
post #9

Earlier quoted context omitted.

If you do this Derived* derived = (Derived*)base; and then use both base and derived, is that not violating aliasing rules?

Pretty sure it's safe! Take this program: typedef struct { int x; } Base; typedef struct { Base base; int y; } Derived; int f(Base* b, Derived* d) { b->x = 0; d->base.x = 1; return b->x; } Notice that if we are accessing the base members of "d", we are still accessing them through a struct of type "Base" (d->base.x). If we compile this with strict aliasing, you can see the output is allowing that the two might alias…

It is defined.

6.5.7. An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

- an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)

This means Derived is allowed to alias Base.

Post reply on HN