59

I'm wondering about the use of code like the following

int result = 0;
int factor = 1;
for (...) {
    result = ...
    factor *= 10;
}
return result;

If the loop is iterated over n times, then factor is multiplied by 10 exactly n times. However, factor is only ever used after having been multiplied by 10 a total of n-1 times. If we assume that factor never overflows except on the last iteration of the loop, but may overflow on the last iteration of the loop, then should such code be acceptable? In this case, the value of factor would provably never be used after the overflow has happened.

I'm having a debate on whether code like this should be accepted. It would be possible to put the multiplication inside an if-statement and just not do the multiplication on the last iteration of the loop when it can overflow. The downside is that it clutters the code and adds an unnecessary branch that would need to check for on all the previous loop iterations. I could also iterate over the loop one fewer time and replicate the loop body once after the loop, again, this complicates the code.

The actual code in question is used in a tight inner-loop that consumes a large chunk of the total CPU time in a real-time graphics application.

del
  • 1,127
  • 10
  • 16
  • 5
    I'm voting to close this question as off-topic because this question should be on https://codereview.stackexchange.com/ not here. – Kevin Anderson Nov 29 '19 at 13:15
  • 31
    @KevinAnderson, no it's valid here, as the example code is to be fixed, not merely improved. – Bathsheba Nov 29 '19 at 13:16
  • @KevinAnderson How is the code broken? It can't be fixed because no problem has been defined. – nicomp Nov 29 '19 at 13:16
  • If posslbe, might as well use unsigned, since there overflow is well-defined. – DeiDei Nov 29 '19 at 13:18
  • @DeiDei: Exactly! – Bathsheba Nov 29 '19 at 13:19
  • https://stackoverflow.com/a/7682539/560648 – Lightness Races in Orbit Nov 29 '19 at 13:36
  • https://stackoverflow.com/a/36629498/560648 – Lightness Races in Orbit Nov 29 '19 at 13:37
  • https://stackoverflow.com/a/48731480/560648 – Lightness Races in Orbit Nov 29 '19 at 13:39
  • As far as the code goes, is there anything wrong with changing `for(;condition;){ ... }` to `while(true){ result = ...; if(!condition) break; factor *= 10; }`? I think that's a pretty typical way to avoid executing part of a loop without adding extra branches or duplicating code. – Milo Brandt Nov 30 '19 at 04:36
  • "Is there UB" and "should this code be accepted" are not the same question. – harold Nov 30 '19 at 09:58
  • 1
    @harold They're dang close. – Lightness Races in Orbit Nov 30 '19 at 16:07
  • 1
    @LightnessRaceswithMonica: The authors of the Standard intended and expected that implementations intended for various platforms and purposes would extend the semantics available to programmers by meaningfully process various actions in ways useful for those platforms and purposes whether or not the Standard required them to do so, and also stated that they did not wish to demean non-portable code. Thus, the similarity between the questions depends on which implementations needs to support. – supercat Nov 30 '19 at 22:52
  • 2
    @supercat For implementation-defined behaviours sure, and if you know your toolchain has some extension you can use (and you don't care about portability), fine. For UB? Doubtful. – Lightness Races in Orbit Nov 30 '19 at 23:13
  • @LightnessRaceswithMonica: The distinction between IDB and UB is whether an all implementations would be required to guarantee the behavior of an action, including those where guaranteeing anything would be expensive, and nothing they could guarantee would be very useful. The published Rationale makes clear the intention that implementations use actions where the Standard imposes no requirements to let programmers do things not provided for in the Standard. – supercat Nov 30 '19 at 23:18
  • 1
    @supercat: UB is for permitting the implementation to do whatever it likes. Not the programmer. – Lightness Races in Orbit Nov 30 '19 at 23:41
  • Why is this not answered by basic research on UB & the meaning of programs? – philipxy Dec 02 '19 at 03:22
  • Does this answer your question? [Why does integer overflow on x86 with GCC cause an infinite loop?](https://stackoverflow.com/questions/7682477/why-does-integer-overflow-on-x86-with-gcc-cause-an-infinite-loop) – philipxy Dec 02 '19 at 03:27
  • 1
    @LightnessRaceswithMonica: It is to allow implementations to do whatever will best serve their customers. Compilers that honor the spirit of C described in the Rationale will refrain from obstructing the programmer Such a philosophy worked until the compiler market was undermined by people more interested in showcasing "cleverness" than actually serving their customers. – supercat Dec 02 '19 at 07:34
  • @supercat Not really sure what you're trying to get at it, but that last sentence suggests you're on the warpath against someone, which isn't really on topic here. – Lightness Races in Orbit Dec 02 '19 at 11:02
  • It can already hit UB in the 5. loop, the only guarantee you have is `INT_MAX>=32785`. Why not use `uint_least32_t` or `unsigned long`? – 12431234123412341234123 Dec 02 '19 at 11:56
  • This question provoked far more discussion than I expected! It fueled a lot of good debate at the office. :) – del Dec 09 '19 at 12:48

10 Answers10

58

Compilers do assume that a valid C++ program does not contain UB. Consider for example:

if (x == nullptr) {
    *x = 3;
} else {
    *x = 5;
}

If x == nullptr then dereferencing it and assigning a value is UB. Hence the only way this could end in a valid program is when x == nullptr will never yield true and the compiler can assume under the as if rule, the above is equivalent to:

*x = 5;

Now in your code

int result = 0;
int factor = 1;
for (...) {      // Loop until factor overflows but not more
   result = ...
   factor *= 10;
}
return result;

The last multiplication of factor cannot happen in a valid program (signed overflow is undefined). Hence also the assignment to result cannot happen. As there is no way to branch before the last iteration also the previous iteration cannot happen. Eventually, the part of code that is correct (i.e., no undefined behaviour ever happens) is:

// nothing :(
463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • 7
    "Undefined Behavior" is a expression we hear a lot in SO answers without clearly explaining how it can affect a program as a whole. This answer makes things a lot clearer. – Gilles-Philippe Paillé Nov 29 '19 at 13:34
  • 1
    And this could even be a "useful optimization" if the function is only called on targets with `INT_MAX >= 10000000000`, with a different function called in the case where `INT_MAX` is smaller. – R.. GitHub STOP HELPING ICE Nov 30 '19 at 14:08
  • 2
    @Gilles-PhilippePaillé There are times where I wish we could stickey a post on that. [Benign Data Races](https://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong) is one of my favorites for capturing how nasty they can be. There's also a great bug report in MySQL that I can't seem to find again -- a buffer overflow check that accidentally invoked UB. A particular version of a particular compiler simply assumed the UB never occurs, and optimized out the entire overflow check. – Cort Ammon Nov 30 '19 at 16:55
  • @CortAmmon-ReinstateMonica I agree with you. Despite 20 years of experience with C++, I always thought that "signed integer overflow is undefined behavior" simply meant "The result of the calculation cannot be defined in the C++ standard since not every CPU behave the same way", not that a whole loop can be discarded because the compiler decided to deny the existence of signed overflow for the sake of optimization. Thanks for the link. (And sorry for the "chatty" comment). – Gilles-Philippe Paillé Nov 30 '19 at 18:08
  • Compilers that recognize no requirements beyond those given by the Standards would be under no obligation to meaningfully process any program for which the Standard imposes no requirements. I'm not sure where the notion that no "valid" programs contain UB comes from, however, as the Standard explicitly states that it makes no attempt to classify programs as valid or invalid. – supercat Nov 30 '19 at 18:17
  • Some days, I think this whole UB thing has been taken too far. I've heard that UB is literally allowed to make monkeys fly out of my butt. But I have yet to hear of even one account of actual monkeys flying. If I'd been an author of the language spec, I'd have argued for a few different levels of undefined behavior, and I'd have argued that numeric overflows (other than divide-by-zero) belonged to the very lowest category---where the _worst_ thing that would be allowed to happen would be that the operation yields some arbitrary or unpredictable value. – Solomon Slow Nov 30 '19 at 21:04
  • 1
    @SolomonSlow: The main situations where UB is controversial are those where parts of the Standard and implementations' documentation describe the behavior of some action, but some other part of the Standard characterizes it as UB. Common practice before the Standard was written had been for compiler writers to process such actions meaningfully except when their customers would benefit from them doing something else, and I don't think the authors of the Standard ever imagined that compiler writers would willfully do anything else. – supercat Nov 30 '19 at 22:48
  • 2
    @Gilles-PhilippePaillé: [What Every C Programmer Should Know About Undefined Behavior](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html) from the LLVM blog is also good. It explains how for example signed-integer overflow UB can let compilers prove that `i <= n` loops are always non-infinite, like `i – Peter Cordes Dec 01 '19 at 09:30
  • @SolomonSlow there are actually different "levels" of "undefined" - undefined behavior is simply the one that lets compiler kinda do whatever it wants ;), but there is also unspecified behavior and implemetation-defined behavior. – Marandil Dec 01 '19 at 23:35
  • @SolomonSlow It goes hand in hand with other parts of C++ design, like templates. Since templates work at the level of source text, compiler optimizations that eliminate code paths that are impossible are useful. Needless to say, they're also horribly horribly not useful when you actually get into trouble with things like sanity checks unexplainedly disappearing :) We're probably not going to get C++ that can do things like parametric polymorphism without templates, and templates in general are already well entrenched in C++. – Luaan Dec 02 '19 at 08:45
  • This might be an interesting thought experiment, but the phrasing might mislead readers into believing the behaviour of the compiler will be what's described in the answer, as opposed to, well, being undefined. – Bernhard Barker Dec 02 '19 at 13:01
  • @Marandil: What term does the C11 Standard use to describe actions which C89 defined in an unambiguous and sometimes-useful fashion for most platforms, and which 99%+ of production-worthy C99 implementation have been configurable to process in such fashion (generally doing so by default), but which the authors of C99 thought might conceivably be difficult to efficiently process predictably on some platforms that would otherwise be capable of practically supporting C99? – supercat Dec 03 '19 at 21:57
  • @BernhardBarker I can only agree. When writing this question I never expected it to catch that much attention. I was pondering about how to reword it, but after careful consideration I decided to not change a word, but add a disclaimer in front – 463035818_is_not_an_ai Feb 09 '21 at 15:40
  • I disagree with the disclaimer. The gcc compiler will sometimes process a function like `unsigned mul_mod_65536(unsigned short x, unsigned short y) { return (x*y) & 0xFFFF; }` in a manner that disrupts the behavior of the caller if `x` exceeds `INT_MAX/y`. That isn't merely a theoretical possibility; it's easily demonstrable and I think the author of gcc view it as an "optimization" rather than a bug. – supercat Feb 10 '21 at 17:00
37

The behaviour of int overflow is undefined.

It doesn't matter if you read factor outside the loop body; if it has overflowed by then then the behaviour of your code on, after, and somewhat paradoxically before the overflow is undefined.

One issue that might arise in keeping this code is that compilers are getting more and more aggressive when it comes to optimisation. In particular they are developing a habit where they assume that undefined behaviour never happens. For this to be the case, they may remove the for loop altogether.

Can't you use an unsigned type for factor although then you'd need to worry about unwanted conversion of int to unsigned in expressions containing both?

Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • 12
    @nicomp; Why not? – Bathsheba Nov 29 '19 at 13:17
  • @Bathsheba OP clearly knows that overflow is undefined. He just wants to make sure that the last multiplication (the one that overflows but never used afterward) is not problematic (For example, it won't crash the software or have other side effects). – Gilles-Philippe Paillé Nov 29 '19 at 13:20
  • 12
    @Gilles-PhilippePaillé: Doesn't my answer tell you that it is problematic? My opening sentence is not there necessarily for the OP, but the wider community And `factor` is "used" in the assignment back to itself. – Bathsheba Nov 29 '19 at 13:21
  • 8
    @Gilles-PhilippePaillé and this answer explains why it is problematic – 463035818_is_not_an_ai Nov 29 '19 at 13:21
  • 1
    @Bathsheba You're right, I misunderstood your answer. – Gilles-Philippe Paillé Nov 29 '19 at 13:30
  • @Gilles-PhilippePaillé: In fairness to you I have made a few edits along the way. – Bathsheba Nov 29 '19 at 13:35
  • 4
    As an example of undefined behavior, when that code is compiled with runtime checks enabled, it would terminate instead of returning a result. Code that requires me to turn off diagnostic functions in order to work is broken. – Simon Richter Nov 30 '19 at 09:38
  • @SimonRichter: The authors of the C Standard have expressly recognized that actions the Standard classifies as Undefined Behavior may be usefully employed by implementations to extend the language, and also expressly recognized that they did not wish to demean code that performed usefully on its intended target, but that was not portable. What evidence is that they intended to classify that they intended their failure to mandate a behaviors as justification for compiler writers to ignore their customers' needs? – supercat Nov 30 '19 at 18:35
  • @supercat: you mean needs like compiling with `gcc -fwrapv` to make this well-defined as 2's complement signed wraparound? In general I agree that C++ tries too hard not to be a portable assembly language sometimes, and could really benefit from Rusts `wrapping_mul` and `wrapping_add` methods so you can ask for safe wraparound on signed or unsigned types when that is what you mean, and you know you're going to not use results that actually overflowed. – Peter Cordes Dec 01 '19 at 09:28
  • @PeterCordes: In the particular case of overflow, one can use `-fwrapv`, but a more general problem is that the authors of gcc make new optimizations "opt out" rather than "opt in", so there's no way of knowing what flags (except perhaps `'-O0`) will be needed to make code usable with future gcc versions. For example, what flag would be neededd to prevent clang or gcc from inferring that given `extern int x,y,*p`; if `*p==x+1,` an access to `*p` won't access `y`? Even if the Standard would provide no means by which a programmer could place `x` and `y` consecutively, ... – supercat Dec 02 '19 at 07:47
  • ...a programmer who defined those symbols in another language (not uncommon when using freestanding implementations) might know that they are consecutive. Further, if `p` is formed by taking the address of `y`, the fact that it happens to equal the address past `x` should not prevent it from being used to access `y`. – supercat Dec 02 '19 at 07:49
  • @supercat: If `p` is or might be legally-derived from `&y`, I don't think a compiler *can* introduce any logic based on `if (p==x+1)`. But yes, if it's legally-derived from scalar `&x` in C terms, I think it is UB in pure ISO C to access `p[1]`. And yes, optimizing based on that (even indirectly in a way you wouldn't expect to bite you) would break any target-specific tricks like creating a known layout for globals. Anyway, yes your general point about hypothetical future optimizations is well-taken; writing *practical* code that's also free of *all* UB is hard. – Peter Cordes Dec 02 '19 at 07:56
  • @PeterCordes: Both gcc and clang do the indicated inference at if they can't see that `p` is derived from `y`, even with `-fno-strict-aliasing`. The Standard was written on the presumption that if some parts of the Standard and an implementation's documentation describe a sometimes-useful behavior, there's no particular need to avoid also characterizing it as UB, because compiler writers will recognize when they should process the behaviors usefully. Even `volatile short x; int main(void) { int y=x; };` would fall afoul of N1570 6.5p7, for example, ... – supercat Dec 02 '19 at 08:04
  • ...because `x` is read after the lifetime of `y` begins, and the stored value of `y` is written after that, but the stored value of `y` is not written by an lvalue expression of type `int` (indeed, no lvalue of type "int" occurs in the entire program!). Note also that the authors of the Standard describe in the Rationale how they would expect commonplace implementations to process something like `unsigned x = ushort1*short2;` for results between `INT_MAX+1u` and `UINT_MAX`. Why would they say that if they didn't expect commonplace compilers to continue working that way when practical? – supercat Dec 02 '19 at 08:07
28

It might be insightful to consider real-world optimizers. Loop unrolling is a known technique. The basic idea of loop unrolling is that

for (int i = 0; i != 3; ++i)
    foo()

might be better implemented behind the scenes as

 foo()
 foo()
 foo()

This is the easy case, with a fixed bound. But modern compilers can also do this for variable bounds:

for (int i = 0; i != N; ++i)
   foo();

becomes

__RELATIVE_JUMP(3-N)
foo();
foo();
foo();

Obviously this only works if the compiler knows that N<=3. And that's where we get back to the original question:

int result = 0;
int factor = 1;
for (...) {
    result = ...
    factor *= 10;
}
return result;

Because the compiler knows that signed overflow does not occur, it knows that the loop can execute a maximum of 9 times on 32 bits architectures. 10^10 > 2^32. It can therefore do a 9 iteration loop unroll. But the intended maximum was 10 iterations !.

What might happen is that you get a relative jump to a assembly instruction (9-N) with N==10, so an offset of -1, which is the jump instruction itself. Oops. This is a perfectly valid loop optimization for well-defined C++, but the example given turns into a tight infinite loop.

MSalters
  • 173,980
  • 10
  • 155
  • 350
10

Any signed integer overflow results in undefined behaviour, regardless of whether or not the overflowed value is or might be read.

Maybe in your use-case you can to lift the first iteration out of the loop, turning this

int result = 0;
int factor = 1;
for (int n = 0; n < 10; ++n) {
    result += n + factor;
    factor *= 10;
}
// factor "is" 10^10 > INT_MAX, UB

into this

int factor = 1;
int result = 0 + factor; // first iteration
for (int n = 1; n < 10; ++n) {
    factor *= 10;
    result += n + factor;
}
// factor is 10^9 < INT_MAX

With optimization enabled, the compiler might unroll the second loop above into one conditional jump.

elbrunovsky
  • 442
  • 5
  • 11
  • 6
    This may be a bit over-technical, but "signed overflow is undefined behavior" is oversimplified. Formally, the behavior of a program with signed overflow is undefined. That is, the standard does not tell you what that program does. It's not just that there's something wrong with the result that overflowed; there's something wrong with the entire program. – Pete Becker Nov 29 '19 at 13:29
  • Or more simply, peel the *last* iteration and remove the dead `factor *= 10;` – Peter Cordes Dec 01 '19 at 09:33
9

This is UB; in ISO C++ terms the entire behaviour of the entire program is completely unspecified for an execution that eventually hits UB. The classic example is as far as the C++ standard cares, it can make demons fly out of your nose. (I recommend against using an implementation where nasal demons are a real possibility). See other answers for more details.

Compilers can "cause trouble" at compile time for paths of execution they can see leading to compile-time-visible UB, e.g. assume those basic blocks are never reached.

See also What Every C Programmer Should Know About Undefined Behavior (LLVM blog). As explained there, signed-overflow UB lets compilers prove that for(... i <= n ...) loops are not infinite loops, even for unknown n. It also lets them "promote" int loop counters to pointer width instead of redoing sign-extension. (So the consequence of UB in that case could be accessing outside the low 64k or 4G elements of an array, if you were expecting signed wrapping of i into its value range.)

In some cases compilers will emit an illegal instruction like x86 ud2 for a block that provably causes UB if ever executed. (Note that a function might not ever be called, so compilers can't in general go berserk and break other functions, or even possible paths through a function that don't hit UB. i.e. the machine code it compiles to must still work for all inputs that don't lead to UB.)


Probably the most efficient solution is to manually peel the last iteration so the unneeded factor*=10 can be avoided.

int result = 0;
int factor = 1;
for (... i < n-1) {   // stop 1 iteration early
    result = ...
    factor *= 10;
}
 result = ...      // another copy of the loop body, using the last factor
 //   factor *= 10;    // and optimize away this dead operation.
return result;

Or if the loop body is large, consider simply using an unsigned type for factor. Then you can let the unsigned multiply overflow and it will just do well-defined wrapping to some power of 2 (the number of value bits in the unsigned type).

This is fine even if you use it with signed types, especially if your unsigned->signed conversion never overflows.

Conversion between unsigned and 2's complement signed is free (same bit-pattern for all values); the modulo wrapping for int -> unsigned specified by the C++ standard simplifies to just using the same bit-pattern, unlike for one's complement or sign/magnitude.

And unsigned->signed is similarly trivial, although it is implementation-defined for values larger than INT_MAX. If you aren't using the huge unsigned result from the last iteration, you have nothing to worry about. But if you are, see Is conversion from unsigned to signed undefined?. The value-doesn't-fit case is implementation-defined, which means that an implementation must pick some behaviour; sane ones just truncate (if necessary) the unsigned bit pattern and use it as signed, because that works for in-range values the same way with no extra work. And it's definitely not UB. So big unsigned values can become negative signed integers. e.g. after int x = u; gcc and clang don't optimize away x>=0 as always being true, even without -fwrapv, because they defined the behaviour.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 2
    I don't understand the downvote here. I mostly wanted to post about peeling the last iteration. But to still answer the question, I threw together some points about how to grok UB. See other answers for more details. – Peter Cordes Dec 01 '19 at 23:33
5

If you can tolerate a few additional assembly instructions in the loop, instead of

int factor = 1;
for (int j = 0; j < n; ++j) {
    ...
    factor *= 10;
}

you can write:

int factor = 0;
for (...) {
    factor = 10 * factor + !factor;
    ...
}

to avoid the last multiplication. !factor will not introduce a branch:

    xor     ebx, ebx
L1:                       
    xor     eax, eax              
    test    ebx, ebx              
    lea     edx, [rbx+rbx*4]      
    sete    al    
    add     ebp, 1                
    lea     ebx, [rax+rdx*2]      
    mov     edi, ebx              
    call    consume(int)          
    cmp     r12d, ebp             
    jne     .L1                   

This code

int factor = 0;
for (...) {
    factor = factor ? 10 * factor : 1;
    ...
}

also results in branchless assembly after optimization:

    mov     ebx, 1
    jmp     .L1                   
.L2:                               
    lea     ebx, [rbx+rbx*4]       
    add     ebx, ebx
.L1:
    mov     edi, ebx
    add     ebp, 1
    call    consume(int)
    cmp     r12d, ebp
    jne     .L2

(Compiled with GCC 8.3.0 -O3)

Evg
  • 25,259
  • 5
  • 41
  • 83
  • 1
    Simpler to just peel the last iteration, unless the loop body is large. This is a clever hack but increases the latency of the loop-carried dependency chain through `factor` slightly. Or not: when it compiles to 2x LEA it's just about as efficient as LEA + ADD to do `f *= 10` as `f*5*2`, with `test` latency hidden by the first `LEA`. But it does cost extra uops inside the loop so there's a possible throughput downside (or at least a hyperthreading-friendliness issue) – Peter Cordes Dec 01 '19 at 09:44
4

You didn't show what's in the parentheses of the for statement, but I'm going to assume it's something like this:

for (int n = 0; n < 10; ++n) {
    result = ...
    factor *= 10;
}

You can simply move the counter increment and loop termination check into the body:

for (int n = 0; ; ) {
    result = ...
    if (++n >= 10) break;
    factor *= 10;
}

The number of assembly instructions in the loop will remain the same.

Inspired by Andrei Alexandrescu's presentation "Speed Is Found In The Minds of People".

Oktalist
  • 14,336
  • 3
  • 43
  • 63
2

Consider the function:

unsigned mul_mod_65536(unsigned short a, unsigned short b)
{
  return (a*b) & 0xFFFFu;
}

According to the published Rationale, the authors of the Standard would have expected that if this function were invoked on (e.g.) a commonplace 32-bit computer with arguments of 0xC000 and 0xC000, promoting the operands of * to signed int would cause the computation to yield -0x10000000, which when converted to unsigned would yield 0x90000000u--the same answer as if they had made unsigned short promote to unsigned. Nonetheless, gcc will sometimes optimize that function in ways that would behave nonsensically if an overflow occurs. Any code where some combination of inputs could cause an overflow must be processed with -fwrapv option unless it would be acceptable to allow creators of deliberately-malformed input to execute arbitrary code of their choosing.

supercat
  • 77,689
  • 9
  • 166
  • 211
1

Why not this:

int result = 0;
int factor = 10;
for (...) {
    factor *= 10;
    result = ...
}
return result;
  • That doesn't run the `...` loop body for `factor = 1` or `factor = 10`, only 100 and higher. You'd have to peel the first iteration *and* still start with `factor = 1` if you want this to work. – Peter Cordes Dec 01 '19 at 20:21
1

There are many different faces of Undefined Behavior, and what's acceptable depends on the usage.

tight inner-loop that consumes a large chunk of the total CPU time in a real-time graphics application

That, by itself, is a bit of an unusual thing, but be that as it may... if this is indeed the case, then the UB is most probably within the realm "allowable, acceptable". Graphics programming is notorious for hacks and ugly stuff. As long as it "works" and it doesn't take longer than 16.6ms to produce a frame, usually, nobody cares. But still, be aware of what it means to invoke UB.

First, there is the standard. From that point of view, there's nothing to discuss and no way to justify, your code is simply invalid. There are no ifs or whens, it just isn't a valid code. You might as well say that's middle-finger-up from your point of view, and 95-99% of the time you'll be good to go anyway.

Next, there's the hardware side. There are some uncommon, weird architectures where this is a problem. I'm saying "uncommon, weird" because on the one architecture that makes up 80% of all computers (or the two architectures that together make up 95% of all computers) overflow is a "yeah, whatever, don't care" thing on the hardware level. You sure do get a garbage (although still predictable) result, but no evil things happen.
That is not the case on every architecture, you might very well get a trap on overflow (though seeing how you speak of a graphics application, the chances of being on such an odd architecture are rather small). Is portability an issue? If it is, you may want to abstain.

Last, there is the compiler/optimizer side. One reason why overflow is undefined is that simply leaving it at that was easiest to cope with hardware once upon a time. But another reason is that e.g. x+1 is guaranteed to always be larger than x, and the compiler/optimizer can exploit this knowledge. Now, for the previously mentioned case, compilers are indeed known to act this way and simply strip out complete blocks (there existed a Linux exploit some years ago which was based on the compiler having dead-stripped some validation code because of exactly this).
For your case, I would seriously doubt that the compiler does some special, odd, optimizations. However, what do you know, what do I know. When in doubt, try it out. If it works, you are good to go.

(And finally, there's of course code audit, you might have to waste your time discussing this with an auditor if you're unlucky.)

Farzad Karimi
  • 770
  • 1
  • 12
  • 31
Damon
  • 67,688
  • 20
  • 135
  • 185