flatfinger
Although the Standard defines the behavior of signed-to-unsigned conversion in a way that would yield the same bit pattern as a two's-complement signed number, some compilers will issue warnings if a signed value is implicitly coerced to unsigned. Adding the extra 18 forms would generally require nothing more than defining an extra 24 macros, which seems like a reasonable way to prevent such issues.
Note that a compiler could be incapable of processing any useful programs whatsoever, and yet still be a "conforming C implementation" if it is capable of processing a deliberately contrived and useless program that exercises the Standard's translation limits. The authors of the Standard even acknowledge that possibility in the Rationale.
The problem is that the authors of the Standard recognized that anyone seeking to sell compilers would treat Undefined Behavior as an invitation to behave in whatever fashion would best meet their customers' needs, but failed to consider that a moderately-decent freely distributable compiler could become popular as a result of being freely distributable without its maintainers having to respect its users.
What is needed is a category of actions where implementations (which I would call "conditionally-defined", where implementations would be required to indicate via "machine-readable" means [e.g. predefined macros, compiler intrinsics, etc.] all possible consequences (one of which would be UB), from which the implementation might choose in Unspecified fashion. If an implementation reports that it may process signed arithmetic using temporary values that may, at the compiler's leisure, be of an unspecified size that's larger than specified, but signed overflow will have no effect other than to yield values that may be larger than their type would normally be capable of holding, then the implementation would be required to behave in that fashion if integer overflow occurs.
In general, the most efficient code meeting application requirements could be generated by demanding semantics which are as loose as possible without increasing the amount of user code required to meet those requirements. Because different implementations are used for different purposes, so single set of behavioral guarantees would be optimal for all purposes. If compilers are allowed to reject code that demands guarantees an implementation doesn't support, then the choice of what guarantees to support could safely be treated as a Quality of Implementation issue, but the behavior of code that claims to support guarantees would be a conformance issue.
1. define a highly-extensible abstraction model, which implementations intended for various purposes should be expected to extend to suit those purpose, or
2. define an abstraction model which is sufficiently complete that programs can do everything that would need to be done, without need for extensions?
Reading the C89 and C99 Rationale documents, it's clear that those standards were intended to meet the former purpose. The way some compilers treat "Undefined Behavior", however, suggests that the maintainers view the Standard as aimed toward the latter purpose.
During the 1980s and 1990s, it was generally cheaper and easier for implementations to extend the Standard's abstraction model by specifying that many actions would be processed "in a documented fashion characteristic of the environment" than it would have been to do anything else, so there was no need to worry about whether the Standard allowed programmers to specify when such behavior was required. That no longer holds true, however.
While it would be reasonable to deprecate code which relies upon such treatment without explicitly demanding it, such deprecation would only make sense if there were a means of demanding such treatment when required. For the Committee to provide such means, however, it would have to reach a consensus as to the purposes for which the Standard's abstraction model is meant to be suitable. Are you aware of any such consensus?
I think the authors of C89 most likely thought that principle was sufficiently obvious that there was no need to expressly state it. Were it not for the Standard's rule forbidding it, an implementation might plausibly have ignored the possibility of an `int` being accessed via an `unsigned`, but I don't think the authors of the Standard imagined that a non-obtuse compiler writer wouldn't allow for the possibility that something like:
void inc_float_bits(float *f)
{
((unsigned*)f)+=1;
}
might affect the stored value of a `float`.The present rules, as written, have absurd corner cases. Given something like:
union U { float f[2]; unsigned ui[2]; } uu;
the Standard would, so far as I can tell, treat as identical the functions test1, test2, and test3 below: float test1(int i, int j)
{
uu.f[i] = 1.0f;
uu.ui[j] += 1;
return uu.f[i];
}
float test2(int i, int j)
{
*(uu.f+i) = 1.0f;
*(uu.ui+j) += 1;
return *(uu.f+i);
}
float evil(unsigned *ui, float *f)
{ *f = 1.0f; *ui += 1; return *f; }
int test2(int i, int j)
{
evil(uu.f+i, uu.ui+j);
}
If a dereferenced pointer to union member type isn't allowed to access the union, the first example would be UB regardless of i and j, but that would imply that non-character arrays within unions are meaningless. If such pointers are allowed to access union objects, then test2 (and the evil function within it) would have defined behavior even when i and j are both zero.BTW, I think any quality compiler should recognize the possiblity of type punning in the first two, though the Standard doesn't actually require either. Neither clang or gcc, however, recognizes the possibility of type punning in the second even though the behavior of the [] operators in the first are defined* as equivalent to the second.
Some additional value would be gained by allowing stores to automatic objects whose address isn't taken to maintain such extra range at their convenience, without any requirement to avoid having such extra range randomly appear and disappear. Provided that a program coerces values into range in when necessary, such semantics would often be sufficient to meet application requirements without having to prevent overflow.
What additional benefits are achieved by granting compilers unlimited freedom beyond that? I don't see any such benefits that would be worth anything near the extra cost imposed on programmers.
int test(int a, int b)
{
int c = a/b;
if (f1())
f2(a,b,c);
}
Should a compiler be required to compute c before calling f1, and thus have to store the value of c across the function call?Better would be to define a set of semantics for loosely-sequenced traps, along with "causality barriers" to ensure that they only occur at tolerable times.
A couple of easy optimizations, for example, that would be available to a smart compiler processing straightforwardly-written code to use automatic overflow checking, but not to one fed code that uses intrinsics:
1. If code computes x=yz, but then never uses the value of x, a compiler that notices that x is unused could infer that the computation could never be observed to produce an arithmetically-incorrect result, and thus there would be no need to check for overflow.
2. If code computes xy/z, and a compiler knows that y=z*2, the compiler could simplify the calculation to x+x, and would thus merely have to check for overflow in that addition. If code used intrinsics, the compiler would have to overflow check the multiplication, which on most platforms would be more expensive. If an implementation uses wrapping semantics, the cost would be even worse, since an implementation would have to perform an actual division to ensure "correct" behavior in the overflow case.
Having a language offer options for the aforementioned style of loose overflow checking would open up many avenues of optimization which would be unavailable in language that only over precise overflow checking or no overflow checking whatsoever.
Otherwise, the question of what behaviors would be tolerable or intolerable is something programmers should know, but implementations cannot. If implementations offer loose behavioral guarantees, programmers can determine if they meet requirements. If an implementation offers no guarantees whatsoever, however, that is not possible.
If the only thing about overflow is that temporary values may hold weird results, and if certain operations upon a "weird" result (e.g. assignment to anything other than an automatic object whose address is never taken) will coerce it into a possibly-partially-unspecified number within type's range, then a program may ensure that behavior will be acceptable regardless of what weird values result from computation.
According to the published Rationale, the authors of C89 would have expected that something like:
unsigned mul(unsigned short x, unsigned short y)
{ return (x*y); }
would on most implementations yield an arithmetically-correct result even for values of (x*y) between INT_MAX+1U and UINT_MAX. Indeed, I rather doubt they could imagine any compiler for a modern system would do anything other than yield an arithmetically-correct result or--maybe--raise a signal or terminate the program. In some cases, however, that exact function will disrupt the behavior of its caller in nonsensical fashion. Do you think such behavior is consistent with the C89 Committee's intention as expressed in the Rationale?The notion that "Undefined Behavior" is good for optimization is misguided and dangerous. What is good for optimization is having semantics that are loose enough to give the compiler flexibility in how it processes things, but tight enough to meet application requirements.
Instead of saying that compilers can do anything they want when their assumptions are violated, it would be far more useful to recognize what they are allowed to do on the basis of certain assumptions. For example, given a piece of code:
long long test1(long long x, int mode)
{
while(x)
x = slow_function_no_side_effects(x);
return x;
}
void long test2(long long x, int mode)
{
x = test1(x);
if (!mode)
x=0;
doSomething(x);
}
It would generally be useful and safe to allow a compiler that determines that no individual action performed by "test1()" could have any side effects may omit the call to "test1()" if its value never ends up being used, without having to prove that the slow function with no side effects will eventually return zero. It is likewise useful and safe to say that if the generated code observes either that the loop exits or that "mode" is zero, it may replace the call "doSomething(x)" with "doSomething(0)". The fact that both optimizations would be safe and useful individually, however, does not imply that it would be safe and useful to allow compilers to change the code for "test2()" so that it calls "doSomething(0)" or otherwise allow code to observe that the value of "x" is zero when mode is non-zero, without regard for whether "test1()" would complete.1. Behave usefully when practical, if given valid data.
2. Do not behave intolerably, even when given maliciously crafted data.
For a program to be considered usable, point #1 may be sometimes be negotiable (e.g. when given an input file which, while valid, is too big for the available memory). Point #2, however, should be considered non-negotiable.
If integer calculations that overflow are allowed to behave in loosely-defined fashion, that will often be sufficient to allow programs to meet requirement #2 without the need for any source or machine code to control the effects of overflow. If programmers have to take explicit control over the effects of overflow, however, that will prevent compilers from making of the any useful overflow-related options that would be consistent with loosely-defined behavior.
Under the kind of model I have in mind, a compiler would be allowed to treat temporary integer objects as being capable of holding values outside the range of their types, which would allow a compiler to optimize e.g. x*y/y to x, or x+y>y to x>0, but the effects of overflow would be limited to the computation of potentially weird values. If a program would meet requirements regardless of what values a temporary integer object holds, allowing such objects to acquire such weird values may be more efficient than requiring that programs write code to prevent computation of such values.
Recognizing common behavioral characteristics would actually improve the usability of arcane hardware platforms if there were ways of explicitly requesting the commonplace semantics when required. For example, if the Standard defined an intrinsic which, given a pointer that is four-byte aligned, would store a 32-bit value with 8 bits per byte little-endian format, leaving the any bits beyond the eighth (if any) in a state which would be compatible with using "fwrite" to an octet-based stream, an octet-based big-endian platform could easily process that intrinsic as a byte-swap instruction followed by a 32-bit store, while a compiler for a 36-bit system could use a combination of addition and masking operations to spread out the bits.
1. Have 10% of implementations support the feature one way, and 10% support it in an incompatible fashion.
2. Require that all compiler writers invest the time and necessary to support the feature without regard for whether any of their customers would ever use it.
3. Specify that implementations may either support the feature or report that they don't do so, at their leisure, but that implementations which claim to support the feature must do so in the manner prescribed by the Standard.
When C89 was written, the Committee decided that rather than recognizing different categories of implementation that support different sets of features, it should treat the question of what "popular extensions" to support as a Quality of Implementation which could be better resolved by the marketplace than by the Committee.
IMHO, the Committee should recognize categories of Safely Conforming Implementation and Selectively Conforming Program such that if an SCI accepts an SCP, and the translation and execution environments satisfy all documented requirements of the SCI and SCP, the program will behave as described by the Standard, or report in Implementation-Defined fashion an inability to do so, period. Any other behavior would make an implementation non-conforming. No "translation limit" loopholes.