C is not as simple as it seems
C is not as simple as it seems
Posted Sep 25, 2024 23:38 UTC (Wed) by NYKevin (subscriber, #129325)In reply to: C is not as simple as it seems by Sesse
Parent article: Linus and Dirk on succession, Rust, and more
* You cannot compare pointers for inequality (<, <=, etc.) unless they both point within the same array or struct. Under a careful read of the standard's precise wording, it *appears* to me that it is even UB if we have an array of structs, one of the pointers points to a field of one of those structs, and the other pointer points to a field of a different struct in the same array (the standard specifies comparisons involving two "structure members" and comparisons involving two "array elements," but does not appear to contemplate a mixture of the two). For example, if we know that foo is a struct with a field called bar, and x and y point to elements of some array of struct foo, then the compiler may optimize &(x->bar) <= &(y->bar) to 1 without regard for the values of x and y (if they both point to the same struct foo, then the expression is true, and if not, then it's UB). Note that this rule does not apply to == and !=, which are required to work correctly in this case. Of course any right-thinking person writes x <= y instead, but this is a toy example. You may hypothesize that the compiler does some level of constant propagation, inlining, and other code movement to get to this case.
* You cannot perform arbitrary arithmetic on pointers - you may only construct other pointers within or one past the end of the same array (but, for the purpose of this rule, an object which is not in any array is considered to belong to a one-element array). Any other arithmetic is UB, regardless of whether you dereference the pointer.
* Subtracting two pointers produces a result of type ptrdiff_t, if the subtraction is legal, but ptrdiff_t is not required to be wide enough to hold all possible pointer subtractions. It can overflow, and that is UB.
* Type punning through a pointer cast is generally UB, even in cases where the object representation is necessarily valid, unless the types are "compatible" or one of them is some variation of char*. But at least in this case, you don't get UB until you actually dereference the pointer... unless the resulting pointer violates the alignment requirements of the target type, which is immediate UB.
* Function pointers are not required to have the same representation as object pointers. Unlike object pointers, the implementation is not required to let you inspect the byte representation of a function through a char* cast (or indeed any other object pointer cast), and functions are not even required to live in the same address space as objects.
* Null pointers are initialized with a literal zero or with the NULL macro (that is defined as either a literal zero, or a literal zero cast to void*), but are not required to have an all-bits-zero representation or to type cast into an integer zero.
* You can cast pointers to integers. Overflow is UB even if the target type is unsigned and you do nothing more with that integer.
* You can cast integers to pointers. Doing so may produce a trap representation, so the only safe way to do this is to start with a pointer and round-trip it through an integer.
* As of C23, you can safely cast a pointer to (u)intptr_t, if the implementation provides it. In practice, you can usually get away with using size_t, but formally the standard does not require them to be wide enough.
* Pointer arithmetic is not required to distribute over conversion to integer. If x has type char* and points to a valid object (not one-past-the-end), then (uinptr_t)(x + 1) is not required to equal ((uintptr_t)x) + 1 (but you're going to have a hard time finding an implementation which makes them unequal!).
