This scratches the surface of why I hope C slowly fades away as the default low-level language. C sounds simple when you look through K&R C. C lets you feel like you understand the stack, ALU and memory. A pointer is just an integer and I can manipulate it like an integer.<p>But the reality is filled with a staggering number of weird special cases that exist because memory doesn't work like a simple flat address space; or the compiler needs to optimise field layouts, loops, functions, allocations, register assignments and local variables; or your CPU doesn't use the lower 4 bits or upper 24 bits when addressing.<p>C has no way to shield common language constructs from these problems. So everything in the language is a little bit compromised. Program in C for long enough and you'll hit a lot of special special cases – usually in the worst way: runtime misbehavior. Type punning corruption, pointers with equal bit representations that are not equal, values that change mysteriously between lines of code, field offsets that are different at debug and release time.<p>When using fundamental language constructs, we shouldn't need to worry about these ugly special cases – but C is built around these ideas. The need to specify memory layouts, direct memory access and other low level access should be gated by barriers that ensure the language's representation and the machine representation don't tread on each other's toes.<p>Rust has a long way to go but is so much more robust at runtime. I think there's room for other languages to occupy a similar space but they're need to focus on no-std-lib no-runtime operation (not always the sexiest target).
Note that if the two pointers are passed to a function, and the comparison is done in the function, the results are different:<p><pre><code> #include <stdio.h>
void pcmp(int *p, int *q)
{
printf("%p %p %d\n", (void *)p, (void *)q, p == q);
}
int main(void) {
int a, b;
int *p = &a;
int *q = &b + 1;
printf("%p %p %d\n", (void *)p, (void *)q, p == q);
pcmp(p, q);
return 0;
}
</code></pre>
That is giving me:<p><pre><code> 0x7ffebac1483c 0x7ffebac1483c 0
0x7ffebac1483c 0x7ffebac1483c 1
</code></pre>
That's compiled with '-std=c11 -O1' as in the article. The result is the same of pcmp is moved into a separate file so that when compiling it the compiler has no knowledge of the origins of the two pointers.<p>I don't like this at all. It bugs me that I can get different results comparing two pointers depending on where I happen to do the comparison.
I like the behavior of the compiler here. There is no guarantee that a and b are next to each other in memory. That's why the comparison fails, the alternative makes is runtime/compiler/optimization level dependent which would be a total mess.<p>As usual with those C bashing articles you won't run into trouble if you don't try very hard to write contrived code.
I mean, the moment you see:<p><pre><code> int *q = &b + 1;
</code></pre>
on your screen alarm bells should go off. Doing pointer arithmetic on something that is not an array is asking for trouble. If the standard should be amended in any way it should be undefined behavior right away you do pointer arithmetic on non-array objects.
the comparison at the start is nonsense - there is no specification for the ordering or location of stack variables. by taking the address of these variables, you could see that they actually are the same value, and so intuitively you’d think they might be the same, but a different compiler might put them in different locations. or they may be elided entirely through optimisation. it’s far safer to fail the equality test in this case - this is what the model specifies.<p>this is not even the first example of this counter-initiative behaviour. imagine two floating point values with exactly the same bit-representation. it is possible, without any trickery for them to fail an equality check - i.e, they are both NaN.<p>this is what IEE754 demands of a compliant floating point implementation. and indeed, it’s a sane choice when you understand why it was made.<p>similarly, it’s perfectly reasonable for this example to fail.
clang is a bit more sane:<p><pre><code> > cc pointereq.c
> ./a.out
0x7ffee83163b8 0x7ffee83163b8 1
> cc -O pointereq.c
> ./a.out
0x7ffeeeb8b3b8 0x7ffeeeb8b3c0 0
</code></pre>
So without optimization, the pointers are the same and compare as equal. With optimization, the pointers compare as not equal. At first that seemed horrible, until I saw the pointers actually are not the same. Since I don't recall any guarantees about stack layout, that seems perfectly fine.<p><pre><code> > cat pointereq.c
#include <stdio.h>
int main(void) {
int a, b;
int *p = &a;
int *q = &b + 1;
printf("%p %p %d\n", (void *)p, (void *)q, p == q);
return 0;
}</code></pre>
There's nothing surprising in the first example. Comparing the addresses of stack variables is undefined behaviour.<p>The second one is more interesting:<p><pre><code> extern int _start[];
extern int _end[];
void foo(void) {
for (int *i = _start; i != _end; ++i) { /* ... */ }
}
</code></pre>
GCC optimized "i != _end" into "true". The kernel guys fixed this by turning "_start" and "_end" into "extern int*". I always thought [] was just syntactic sugar over a regular pointer, but seems like I was wrong.
<i>Two pointers compare equal if and only if both are null pointers, both are pointers to the same object (including a pointer to an object and a subobject at its beginning) or function, both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.</i><p>Can someone explain to me the rationale behind this?
Why not just "two pointers compare equal if they point to the same address"?
When I compile the example, i get:<p><pre><code> 0x7ffd0b57ebd0 0x7ffd0b57ebd8 0
</code></pre>
OK, so gcc reordered a & b; I'll fix this by chaning the initialisation of p and q to:<p><pre><code> int *p = &a + 1;
int *q = &b;
</code></pre>
But when I now run the example, I get:<p><pre><code> gcc -o c c.c && ./c
0x7ffe55eb0aa4 0x7ffe55eb0aa4 1
gcc -O -o c c.c && ./c
0x7ffcfeffd914 0x7ffcfeffd914 0
</code></pre>
So, p==q only evaluates to 1 if optimisation is enabled.<p><pre><code> $ gcc --version
gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0</code></pre>
> If we step back from the standard and ask our self does it make sense to compare two pointers which are derived from two completely unrelated objects? The answer is probably always no.<p>The one big counterexample I can think of is the difference between memcpy and memmove. The latter is supposed to be able to do arithmetic on memory regions, to see if they overlap. Is this article saying that the standard C implementation of memmove is relying on unspecified behavior?
> <i>...or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.</i><p>I was not aware of this special case. What's the rationale? Is there even a way in standard C to guarantee that two array objects are laid out in memory like that, with no padding?