C / OPERATORS AND EXPRESSIONS
Comparison operators and testing floats safely
Compare integers and floats correctly in C: know what a < b evaluates to, avoid == on doubles, and write a tolerance test that handles NaN and scale.
What you will learn
- Read a < b as an int expression that yields 0 or 1, never as a boolean type
- Write 0 <= x && x <= 10, because (0 <= x) <= 10 is true for every x
- Test doubles with a relative tolerance plus an absolute floor, not with ==
- Check isnan(x) first: every comparison involving NaN, including x == x, is false
Understanding Comparison operators and testing floats safely
C has six comparison operators, and none of them produces a boolean type: <, >, <=, >=, == and != all evaluate to an int that is exactly 1 or 0, which is why printf("%d", a < b) and count += (a > 10) are ordinary code. Before the comparison happens both operands go through the usual arithmetic conversions, so what gets compared is not always the value you wrote: -1 < 1u is false because the -1 is converted to a huge unsigned value first. The four relational operators bind tighter than == and !=, and all six are left-associative, so 0 <= x <= 10 parses as (0 <= x) <= 10, whose left half collapses to 0 or 1 and both of those are <= 10.
A double stores a sign, an exponent and 53 significant binary digits, so the only values it can hold exactly are of the form m times a power of two. 0.1, 0.2 and 0.3 are not such values, so the compiler substitutes the nearest representable neighbours, and every arithmetic step rounds its true result to the nearest representable value again. That is why 0.1 + 0.2 lands one representable step above the neighbour chosen for 0.3, a difference of 5.55e-17, and == honestly reports that they are two different numbers. The defect is in the question, not the hardware: == asks about exact identity, and the two sides got to their values by different rounding paths.
The useful question is whether two values are nearer than you care about, and the tolerance has to come from the magnitudes involved. A fixed fabs(a - b) <= 1e-9 degenerates into == somewhere around a billion, where consecutive doubles are already about 1.2e-7 apart, and near 1e-20 it calls everything equal; scaling the tolerance by the larger magnitude fixes the big end but collapses to zero at zero, so real code combines a relative tolerance with a small absolute floor. DBL_EPSILON from float.h is the width of one step at 1.0, about 2.2e-16, which makes it a unit for expressing relative error rather than a constant to paste into every comparison. NaN then breaks the last assumption you have left: it compares false against everything including itself, so !(a < b) is not equivalent to a >= b, and isnan(x) is how you find it.
<stdio.h>
<math.h>
/* Equal to within a relative tolerance, plus an absolute floor so that
values very close to zero can still compare as equal. */
static int nearly_equal(double a, double b, double rel, double abs_tol)
{
double diff = fabs(a - b);
double scale = fabs(a) > fabs(b) ? fabs(a) : fabs(b);
if (diff <= abs_tol)
return 1;
return diff <= rel * scale;
}
int main(void)
{
double sum = 0.1 + 0.2;
double missing = NAN;
printf("0.1 + 0.2 = %.17g\n", sum);
printf("0.3 = %.17g\n", 0.3);
printf("sum == 0.3 : %d\n", sum == 0.3);
printf("gap : %.3e\n", fabs(sum - 0.3));
printf("nearly_equal : %d\n", nearly_equal(sum, 0.3, 1e-12, 1e-300));
printf("value of (3 < 5) : %d\n", 3 < 5);
printf("missing == missing: %d\n", missing == missing);
printf("missing != missing: %d\n", missing != missing);
printf("isnan(missing) : %d\n", isnan(missing) != 0);
return 0;
}
A comparison in C is an int-valued test on converted operands, and for floating point the only meaningful test is whether the difference is small relative to the magnitudes involved.
Worked examples
Comparisons are ints, and they do not chain
Shows why a chained range test always succeeds and how the 0/1 result can be used in arithmetic.
<stdio.h>
int main(void)
{
int x = 42;
printf("0 <= x <= 10 : %d\n", 0 <= x <= 10);
printf("0 <= x && x <= 10 : %d\n", 0 <= x && x <= 10);
printf("thresholds passed : %d\n", (x > 10) + (x > 40) + (x > 100));
return 0;
}
Example explained
Line 10 <= x <= 10 groups as (0 <= 42) <= 10; the inner test yields 1, and 1 <= 10 is true, so the range check reports success for 42.
Line 2The && form asks both questions about x itself, so it correctly reports 0.
Line 3(x > 10) + (x > 40) + (x > 100) adds 1 + 1 + 0 because each comparison is an int, so arithmetic on comparison results is well defined.
Line 4GCC can flag this exact shape under -Wall (-Wbool-compare) because 10 is a literal; replace 10 with a variable and the warning disappears.
Ten additions of 0.1 versus one multiplication
Demonstrates that error comes from the number of rounding steps, not from the constant 0.1 itself.
<stdio.h>
int main(void)
{
double sum = 0.0;
for (int i = 0; i < 10; i++)
sum += 0.1;
printf("sum of ten 0.1 = %.17g\n", sum);
printf("sum == 1.0 : %d\n", sum == 1.0);
printf("1.0 - sum = %.3e\n", 1.0 - sum);
printf("10 * 0.1 = %.17g\n", 10 * 0.1);
printf("product == 1.0 : %d\n", 10 * 0.1 == 1.0);
return 0;
}
Example explained
Line 1Each += rounds the intermediate total to the nearest double, and after ten of those roundings the sum sits one step below 1.0, so == 1.0 is false.
Line 2%.17g is what makes the miss visible; printing with %f would show 1.000000 and hide it completely.
Line 310 * 0.1 does a single multiplication whose exact result is only a quarter of a step above 1.0, so it rounds to exactly 1.0 and the comparison is true.
Line 4The rule to take away is that the tolerance you need grows with the number of operations, not with the number of digits you typed.
A comparison converts before it compares
Shows how mixing a signed and an unsigned operand silently changes the meaning of <.
<stdio.h>
<limits.h>
int main(void)
{
int t = -1;
unsigned limit = 1;
printf("t < limit : %d\n", t < limit);
printf("(unsigned)t == UINT_MAX : %d\n", (unsigned)t == UINT_MAX);
printf("t < (int)limit : %d\n", t < (int)limit);
return 0;
}
Example explained
Line 1int and unsigned int have the same rank, so the usual arithmetic conversions turn t into unsigned; -1 becomes the largest unsigned value, which is not less than 1.
Line 2The second line confirms that conversion: (unsigned)-1 equals UINT_MAX, which is 4294967295 wherever int is 32 bits.
Line 3Casting the unsigned operand to int makes both sides signed and restores the arithmetic meaning of the test.
Line 4Compiling with -Wextra reports the first line as a sign-compare warning; treat it as a bug rather than noise.
Important notes
-0.0 == 0.0 is true even though the two have different bit patterns, so == cannot tell you the sign of a zero; use signbit() when that matters.
A tolerance test is not transitive: a can be near b and b near c while a and c fail, so never use it as the equality relation behind sorting, deduplication or lookup keys.
Common mistakes
Writing if (0 <= x <= 10) as a range check: it compiles, and it is true for every x, so the guard lets every value through without a runtime error to show it.
Driving a loop with while (x != 1.0) x += 0.1;: after ten additions x is 0.99999999999999989, so the exit test never fires and the loop runs forever.
Hard-coding one tolerance such as fabs(a - b) < 1e-9 for all magnitudes: near a billion one representable step is already about 1.2e-7, so the test is stricter than the type allows and only bit-identical values pass.
Try it yourself
Change, predict, then run
Write nearly_equal(a, b) with a 1e-12 relative tolerance and a 1e-15 absolute floor, then print %d for sqrt(2.0) * sqrt(2.0) == 2.0 and for nearly_equal(sqrt(2.0) * sqrt(2.0), 2.0). Also print the product with %.17g to see which side of 2.0 it landed on.
Open the C workspaceCheck your understanding
A helper reports 1 when fabs(a - b) <= 1e-9. It behaves sensibly for values around 1.0, but for two measurements near 5e12 that an engineer would call identical it keeps returning 0. What is going on?
- The subtraction a - b overflows once the operands grow past about 1e12
- 1e-9 has no exact double representation, so the comparison result is unreliable
- Near 5e12 consecutive doubles are already about 0.001 apart, so nothing short of a bit-for-bit match can pass a 1e-9 test
- fabs() only returns accurate results for arguments smaller than 1.0
Show answer
A double keeps 53 significant bits, so the gap between neighbours scales with the value: near 5e12 that gap is about 0.001, and any two distinct doubles there differ by at least that much, which makes a 1e-9 threshold behave exactly like ==. The representation of 1e-9 is a tempting answer because 1e-9 genuinely is not exact in binary, but its error is around 1e-25 and irrelevant here; overflow is also out of the question since 5e12 is far below DBL_MAX. Scale the tolerance by the magnitude of the operands instead.