I used to think that, but I no longer do. What changed my mind is that C23 redefined realloc(ptr, 0) to be UB, with the explicit purpose of allowing downstream standards to refine the contract:
Classifying a call to realloc with a size of 0 as undefined behavior would allow POSIX to define the
otherwise undefined behavior however they please.
So, I think, after all UB means what it says on the tin: just a mere lack of any requirements whatsoever. This absence of guarantees can be used for different purposes: for optimization, to make compilation possible at all (it is a fallacy to think that non optimizing compiler somehow dodges UB), or, as in the realloc case, to define the behavior elsewhere.
I wish that everyone adopted “Checked Illegal Behavior” and “Unchecked Illegal Behavior” terminology instead. It just more precisely expresses the phenomenon we care about with respect to memory safety and all that stuff. Certain runtime behavior of the program are “bad” simply because they are deemed to be so by fiat. It’s just the letter of the law, without obligatory connection to hardware.
This legalistic view makes it easy to see the role of compiler as a translator between the high-level language contract and the low-level language contract.
Classifying a call to realloc with a size of 0 as undefined behavior would allow POSIX to define the otherwise undefined behavior however they please.
At least at first blush this sounds like a perfect match for implementation-defined behavior, so I’m rather puzzled why the committee sprung for UB instead. I’d assume that that was discussed in meetings and/or on the mailing list but unfortunately the paper does not elaborate further :(
In C89, calling realloc(ptr,0) (assuming ptr had been allocated previously) would actually free the memory. This changed in C99, so realloc(ptr,0) would return a pointer to 0 bytes [1]. I think so many programs assumed the C89 behavior that the C99 behavior probably caught most programmers by surprise.
[1] What does allocating 0 bytes even mean? A valid pointer that can’t be used?
I would hesitate to say that U(ndefined)B exists for the purpose of compiler optimization. You can have a Turing-decidable language and still optimize within it, and that requires the decider to be total, so the semantics of every behavior have to be well-defined by definition. All you need is a notion of what behavior is and is not observable.
My personal crackpot theory as to why the term “undefined behavior” has stuck around is aversion to the idea of “nonstandard behavior.” Crashing when pointers to different types alias is often way better than introducing memory corruption via ordering, or worse, coherence mismatches. If this was deemed “checked illegal behavior,” and this program blew up the planet:
That may end up being a nightmare for someone. However, if the behavior is undefined, then it is perfectly legal for the compiler to decide to blow up the planet.
(to clarify, my claim is “can be used for”, not “exists for”, I am agnostic about UB’s original intended purpose)
All you need is a notion of what behavior is and is not observable.
That’s the issue for C though, everything is observable, and why I think any discussion about optimization in the context of C UB is ultimately a red herring. Consider:
int a = 1;
int b = 2;
int* a_ptr = &a;
int* feeling_lucky_ptr = a_ptr + 1;
*feeling_lucky_ptr += 1;
printf("%d", b);
You just can’t explain what a non optimizing compiler would do to this program, without some notion of (Unchecked) Illegal Behavior.
Are you proposing the standard itself imposes what is and is not checked? Or just what is illegal? If the former, you still run into the problem I proposed if you ever deem any behavior to be “checked illegal.” In the latter case, why is “maybe checked illegal behavior” practically any better than “undefined,” if the answer as to what happens is still “I dunno?”
That’s the issue for C though, everything is observable
And as far as I understand it, please correct me if I am mistaken, this is not true. Just because you gave a a memory address does not mean that its value is necessarily observable.
If what you mean by “unoptimizing compiler” is a naive compiler that translates the code to the letter like a macro assembler, then you could very easily explain what that code does, just not through the lens of the abstract machine. It writes happens to be at a_ptr + 1, which is a perfectly reasonable statement given that the abstract machine is not in question here.
To loop back, is the behavior I just described “non-standard” (not explained by the standard), “undefined” (also not explained by the standard), or “unchecked and illegal” (I don’t know exactly what this means)? Practically, what is the difference?
Classifying a call to realloc with a size of 0 as undefined behavior would allow POSIX to define the otherwise undefined behavior however they please.
What posix says is irrelevant though. realloc is specified as part of the language, compilers are aware of the semantics, and the platform providing defined behavior is irrelevant. If the intent is to permit a platform to specified the contract for realloc, the solution is implementation defined or unspecified behavior, not to label it UB.
Labeling it UB means that
void *foo(void* ptr, int newsize) {
if (!newsize) report("zero sized realloc")
return realloc(ptr, newsize);
}
can be optimized to
void *foo(void* ptr, int newsize) {
return realloc(ptr, newsize);
}
If the compiler discovers that newsize is zero, this can be compiled to
_foo:
_some_other_function:
...
That’s literally the definition of what UB allows. The underlying implementation is completely irrelevant, by saying realloc(ptr, 0) is UB they are not saying “the implementation can change the contract for a zero sized realloc”, they are saying that the behavior of an implementation in response to a zero sized realloc is irrelevant.
More realistically a compiler might choose to lower realloc(.., 0) or equivalently malloc(0) to a trap: it’s UB, so it is permitted to do so, and malloc(0) and realloc(…, 0) are generally considered bad things to do for a variety of reasons. So trapping when that happens means that the mistake is identified early (it is definitionally incorrect code), helping the developer fix it, and it helps the implementation by removing the need to support it. This applies even if the implementation intentionally supports it.
You cannot have a definition of UB in which there are no constraints on how a compiler treats signed integer overflow, while also saying that their are constraints on how realloc(ptr, 0) is interpreted. If a standard wants to say “the implementation can define the behavior in this case”, the solution is to say it is implementation defined.
I used to think that, but I no longer do. What changed my mind is that C23 redefined
realloc(ptr, 0)to be UB, with the explicit purpose of allowing downstream standards to refine the contract:https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf
So, I think, after all UB means what it says on the tin: just a mere lack of any requirements whatsoever. This absence of guarantees can be used for different purposes: for optimization, to make compilation possible at all (it is a fallacy to think that non optimizing compiler somehow dodges UB), or, as in the realloc case, to define the behavior elsewhere.
I wish that everyone adopted “Checked Illegal Behavior” and “Unchecked Illegal Behavior” terminology instead. It just more precisely expresses the phenomenon we care about with respect to memory safety and all that stuff. Certain runtime behavior of the program are “bad” simply because they are deemed to be so by fiat. It’s just the letter of the law, without obligatory connection to hardware.
This legalistic view makes it easy to see the role of compiler as a translator between the high-level language contract and the low-level language contract.
score 1 · canonical
deleted by creator
At least at first blush this sounds like a perfect match for implementation-defined behavior, so I’m rather puzzled why the committee sprung for UB instead. I’d assume that that was discussed in meetings and/or on the mailing list but unfortunately the paper does not elaborate further :(
score 1 · canonical
In C89, calling
realloc(ptr,0)(assumingptrhad been allocated previously) would actually free the memory. This changed in C99, sorealloc(ptr,0)would return a pointer to 0 bytes [1]. I think so many programs assumed the C89 behavior that the C99 behavior probably caught most programmers by surprise.[1] What does allocating 0 bytes even mean? A valid pointer that can’t be used?
score 1 · canonical
I would hesitate to say that U(ndefined)B exists for the purpose of compiler optimization. You can have a Turing-decidable language and still optimize within it, and that requires the decider to be total, so the semantics of every behavior have to be well-defined by definition. All you need is a notion of what behavior is and is not observable. My personal crackpot theory as to why the term “undefined behavior” has stuck around is aversion to the idea of “nonstandard behavior.” Crashing when pointers to different types alias is often way better than introducing memory corruption via ordering, or worse, coherence mismatches. If this was deemed “checked illegal behavior,” and this program blew up the planet:
volatile int q; void foo(int *a, float *b) { *a = 5; *b = 6; } int main() { foo(&q, (float*)&q); blow_up_the_planet(); }That may end up being a nightmare for someone. However, if the behavior is undefined, then it is perfectly legal for the compiler to decide to blow up the planet.
score 1 · canonical
(to clarify, my claim is “can be used for”, not “exists for”, I am agnostic about UB’s original intended purpose)
That’s the issue for C though, everything is observable, and why I think any discussion about optimization in the context of C UB is ultimately a red herring. Consider:
int a = 1; int b = 2; int* a_ptr = &a; int* feeling_lucky_ptr = a_ptr + 1; *feeling_lucky_ptr += 1; printf("%d", b);You just can’t explain what a non optimizing compiler would do to this program, without some notion of (Unchecked) Illegal Behavior.
score 1 · canonical
Are you proposing the standard itself imposes what is and is not checked? Or just what is illegal? If the former, you still run into the problem I proposed if you ever deem any behavior to be “checked illegal.” In the latter case, why is “maybe checked illegal behavior” practically any better than “undefined,” if the answer as to what happens is still “I dunno?”
And as far as I understand it, please correct me if I am mistaken, this is not true. Just because you gave
aa memory address does not mean that its value is necessarily observable.If what you mean by “unoptimizing compiler” is a naive compiler that translates the code to the letter like a macro assembler, then you could very easily explain what that code does, just not through the lens of the abstract machine. It writes happens to be at
a_ptr + 1, which is a perfectly reasonable statement given that the abstract machine is not in question here.To loop back, is the behavior I just described “non-standard” (not explained by the standard), “undefined” (also not explained by the standard), or “unchecked and illegal” (I don’t know exactly what this means)? Practically, what is the difference?
score 1 · canonical
What posix says is irrelevant though. realloc is specified as part of the language, compilers are aware of the semantics, and the platform providing defined behavior is irrelevant. If the intent is to permit a platform to specified the contract for realloc, the solution is implementation defined or unspecified behavior, not to label it UB.
Labeling it UB means that
can be optimized to
If the compiler discovers that newsize is zero, this can be compiled to
That’s literally the definition of what UB allows. The underlying implementation is completely irrelevant, by saying
realloc(ptr, 0)is UB they are not saying “the implementation can change the contract for a zero sized realloc”, they are saying that the behavior of an implementation in response to a zero sized realloc is irrelevant.More realistically a compiler might choose to lower
realloc(.., 0)or equivalentlymalloc(0)to a trap: it’s UB, so it is permitted to do so, and malloc(0) and realloc(…, 0) are generally considered bad things to do for a variety of reasons. So trapping when that happens means that the mistake is identified early (it is definitionally incorrect code), helping the developer fix it, and it helps the implementation by removing the need to support it. This applies even if the implementation intentionally supports it.You cannot have a definition of UB in which there are no constraints on how a compiler treats signed integer overflow, while also saying that their are constraints on how
realloc(ptr, 0)is interpreted. If a standard wants to say “the implementation can define the behavior in this case”, the solution is to say it is implementation defined.score 1 · canonical