6 ms·
I thought this article was unnecessarily dire. One section claims "Physical Subtyping is Broken", where "physical subtyping" is defined as "the struct-based im
by haberman 11y ago
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/ https://www.python.org/dev/peps/pep-3123/
- TorKlingberg 11y agoIf you do this Derived* derived = (Derived*)base; and then use both base and derived, is that not violating aliasing rules?
- haberman 11y agoPretty 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
- vc98mvco 11y agoIt 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.
- TorKlingberg 11y agoThanks, this is good to know.
- _yosefk 11y agoI 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...)