Undefined Behavior during signed integer overflow in C arithmetic
0 reputation · 19 Oct 2021, 12:01 UTC
0 reputation · 19 Oct 2021, 12:01 UTC
The C standard defines signed integer overflow as undefined behavior (UB), which allows modern compilers to optimize code by assuming such overflows never occur. This creates a challenge when designing systems that require predictable wrapping behavior for signed types.
While unsigned integers are guaranteed to wrap according to the laws of modulo arithmetic, signed integers do not have this guarantee. Relying on a specific hardware implementation of two's complement wrapping can lead to inconsistent binary outputs across different optimization levels (e.g., -O2 vs -O3) or different compiler versions.
Given these constraints, what is the most portable method to detect a pending signed overflow before it occurs without triggering UB? Can this be achieved using standard C types without resorting to compiler-specific intrinsics?
33775 reputation · 19 Oct 2021, 23:35 UTC
The most portable way to know whether a signed arithmetic operation will overflow is to perform the test in a context that is guaranteed to be defined, then map the result back to the original type. Two common patterns work on all mainstream C implementations:
<limits.h> (e.g., INT_MAX, INT_MIN).intmax_t, carry out the operation, and compare the result against the original type’s limits.For two int operands a and b, the following conditions are guaranteed to be defined because they involve only values that fit in an int and comparisons:
bool will_overflow_add(int a, int b) {
if (b > 0) {
return a > INT_MAX - b;
}
if (b < 0) {
return a < INT_MIN - b;
}
return false; /* b == 0 */
}
bool will_overflow_sub(int a, int b) {
/* a - b == a + (-b) */
return will_overflow_add(a, -b);
}
These checks never invoke undefined behavior because they only perform comparisons and subtractions that stay within the bounds of int.
Multiplication is trickier because the product may not fit in the original type even before we evaluate it. The safest pattern is to cast the operands to intmax_t, which is defined to be the widest signed integer type available on the implementation:
bool will_overflow_mul(int a, int b) {
intmax_t aa = a;
intmax_t bb = b;
intmax_t prod = aa * bb;
return prod < INT_MIN || prod > INT_MAX;
}
Because intmax_t can hold any value that an int could produce, the multiplication itself is defined. The final comparison is also defined.
Replace int with the desired type (e.g., long, int64_t) and use the corresponding limits (LONG_MAX, INT64_MAX). For multiplication, cast to intmax_t (or a wider type if available) before multiplying.
intmax_t is the same width as the original type, the cast‑to‑larger‑type trick cannot be used. In that situation you must fall back to manual pre‑checks that avoid any expression that could overflow.<limits.h> when using the limit macros, and test the functions with boundary values such as INT_MAX, INT_MAX-1, INT_MIN, and negative extremes.Compile and run the following test harness to confirm that the helper functions behave as expected:
#include <stdio.h>
#include <limits.h>
#include <stdbool.h>
/* Insert helper functions from above here */
int main(void) {
assert(!will_overflow_add(INT_MAX - 1, 1));
assert(will_overflow_add(INT_MAX, 1));
assert(!will_overflow_mul(2, INT_MAX / 2));
assert(will_overflow_mul(2, INT_MAX));
printf("All checks passed.\n");
return 0;
}
Running this under -Wall -Wextra -O2 should produce no warnings and the program should exit with "All checks passed." if the logic is correct.
By performing pre‑checks against limits.h or by casting to intmax_t before executing the operation, you can detect a pending signed overflow in a fully portable, defined‑behaviour manner. These patterns work across compiler versions and optimization levels without relying on any intrinsics or non‑standard extensions.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.