Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

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 <stdio.h>
     
    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 <copy_8_bytes>:
       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/



I'm with you. I wrote the code that breaks this stuff in gcc (and the implementation of struct-sensitive pointer analysis).

It explicitly and deliberately follows the first member rule, as it should :)

In C++, this is covered by 6.5/7, and allowed because it's a type compatible with the effective type of the object (in a standard layout class, a pointer to the a structure object points to the initial member)


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;


About the memcpy stuff, when you consider the whole picture, this is ridiculous though. There should be no reason for any sane implementation to ever do nasty stuff about

    void copy_8_bytes(char *dst, const char *src) {
      *(uint64_t*)dst = *(uint64_t*)src;
    }
Only maybe excuses. And poor ones.

Now I don't remember the article where I saw that, but technically given the current orientation of compiler writers there are some even more ridiculous situations. Like (a<<n) | (a>>(32-n)) having the obviously desired effect on all current architectures when you look at what would be an obvious direct translation (and quite efficient one already), and yet given the current orientation of compiler writers I would not like to see that code AT ALL unless it is proved that n is always strictly between 1 and 31. And now if they want to restore any kind of efficiency after all that madness, they would have to implement yet another case of convoluted peephole optim. Stupid. Give me my original intent of the langage back, because virtually everybody is using it like that consciously or not, and that will just not change.


How about the fact that the addresses might not be aligned?

How about the fact that there is no reason to write that if what you actually mean is memcpy(dst, src, 8)? Chunking yourself is a premature optimization that the compiler is in a better position to actually perform.


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 <f>:
       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


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.


Thanks, this is good to know.


Josh, what's your opinion about this situation?

https://goo.gl/3hz0em


It's undefined. But the same thing would be undefined in C++, a language that has inheritance built-in: https://goo.gl/shOJi1

You can't downcast to a derived type if the object isn't actually an instance of the derived type. That seems straightforward, no?


It doesn't seem straightforward to me: you're using words like base and derived that aren't in the C standard.


If we talk in terms of concepts that exist in the C standard, we would say that you can't cast an object to pointer-to-X unless your pointer actually points to an X.

The reason your example is illegal is that you are casting to pointer-to-"struct derived", but the thing being pointed to is not actually a "struct derived."

The "physical subtyping" pattern works because the C standard says that a pointer to a struct, suitably converted, also points to its first member. So a pointer-to-Derived, converted to a pointer-to-Base, points at Derived's first member. But a pointer-to-Base doesn't point at a Derived unless that object actually is a Derived. So the downcast is only legal if the object actually is a Derived.


Looks like your other comment hit the max reply depth so this will need to finish up, but in any case I don't agree with your reading of the vice versa.


It may be that the aliasing rules are also required to fully justify my conclusion (ie. a Base can't have its stored value accessed via a pointer-to-derived due to the aliasing rules). But I have a very high degree of confidence in the conclusion itself. I think that you will find that your compiler implements the behavior I have described.


Replying to myself because depth limit.

Let me try to think of a good way to update the post to capture this better...


There isn't actually a depth limit (or if there is we haven't hit it yet :). HackerNews just hides the "reply" link for 5 minutes or so to cool down flamewars.

You can work around this by clicking on the link for the post itself (ie. "3 minutes ago") which allows you to reply immediately.


I don't see text that justifies your one-way argument, the bit of 6.2.7.1 that we are talking about says "and vice versa".


I'm not making a one-way argument. If the underlying object actually is a Derived, you can freely cast between pointer-to-Base and pointer-to-Derived. That is what "and vice versa" means.

But if the object isn't actually a Derived, you can't cast to pointer-to-Derived:

    Derived derived;

    Derived *pDerived = &derived;

    // This is legal because it's equivalent to:
    //   Base *pb = &derived.base;
    //
    // ie. there actually is a Base object there that the
    // pointer is pointing to.
    Base *pBase = (Base*)pDerived;

    // This is legal because pBase points to the initial member
    // of a Derived.  So, suitably converted, it points at the
    // Derived.
    //
    // The key point is that there actually is a Derived object
    // there that we are pointing at.
    pDerived = (Derived*)pBase;

    Base base;

    // This is illegal, because this base object is not actually
    // a part of a larger Derived object, it's just a Base.
    // So we have a pDerived that doesn't actually point at a
    // Derived object -- this is illegal.
    pDerived = (Derived*)&base;

    // Imagine if the above were actually legal -- this would
    // reference unallocated memory!
    pDerived->some_derived_member = 5;


Are you sure about 'illegal' there? Is any compiler going to complain?

All the compilers I have used will cheerfully reference unallocated memory; I thought the behavior was undefined.


When I say "illegal" here, read "undefined behavior." Since undefined behavior is so potentially disastrous, I consider it basically illegal.


To my mind, 'illegal' means that the compiler will complain. In this case, I don't even see weird, scary UB; this is just a case of the standard being completely unable to say anything about what will happen.

After spending too much of my life chasing these bugs, here the compiler will do exactly what you told it to, which probably means making your day miserable.


I'm glad I don't have to understand any of this


That is undefined behavior for two reasons. You're interpreting an object as an object that has an incompatible type.

It also violates string aliasing because 6.5.7. only works for one way. See my other comment.

Derived can alias Base, but not vice-versa.


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.


> You can't cast a character array to any type and dereference it.

So making your own malloc backed by a static char array is undefined behavior?


Yes, I believe it would be. That's a good point, now that you mention it -- I have code that does just that, and I hadn't realized it's probably undefined.

The "effective type" (this is a term defined in the standard) of the char array elements would be "char", whereas the memory returned from malloc() is considered to be an object that initially has no effective type. I don't know of any way to take the char array and "erase" its effective type so that it can be used generically, like the value returned from malloc().

This is a problem!


This is one reason among many why people who write serious low-level code (e.g. game developers) think all the new aliasing rules are completely bonkers.

We implement our own allocators all the time. If you can't even do such a basic thing legally, then the rules are obvious nonsense.


Did you even make sure the memory was aligned?

Non-allocated memory (stack char) cannot be used like that. I'm sure you know alloc(), and malloc() is fast for small sizes.


> Did you even make sure the memory was aligned?

Of course, what do you take me for? :)

> I'm sure you know alloc()

Do you mean alloca? It has a lot of problems, and is generally prohibited at Google.

> and malloc() is fast for small sizes.

Not nearly fast enough for my purposes.


> Just making sure. Fix that code will you! :)

> Yes, alloca. (Don't use it.)

> Just make a fast allocator that uses heap instead of the stack. You only need to malloc once and it can be used for any type since like you pointed out, it's effective type can be changed.


Sometimes, especially in embedded systems, it is useful to have a bunch of statically allocated heaps. You can see them in a memory map, and the linker will tell you if they don't fit in memory.

There is also the case where you have some raw data from a file or network, that you want to re-interpret as a struct. That is always dangerous with endianness and struct padding, but it is a very common practice. You could always memcpy from a char array to a struct, but that can waste memory.


Ok, what's wrong with alloca? Other than blowing the stack, that is.


See: https://google.github.io/styleguide/cppguide.html#Variable-L...

At least that's the rationale for why we don't use it at Google.


"More importantly, they allocate a data-dependent amount of stack space that can trigger difficult-to-find memory overwriting bugs: "It ran fine on my machine, but dies mysteriously in production"."

Yep, blowing the stack will do that.




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: