Skip to Content
Course content

115: Floating-Point Limits with float.h

Click on the "Edit" button in the top corner of the screen to edit your slide content.

One of the most frustrating things for a developer moving into systems programming is realizing that floating-point numbers aren't actually numbers—they're approximations. I can't tell you how many times I've seen a junior dev spend an entire afternoon debugging a loop that never terminates because they were checking if a double exactly equaled 1.0. In the real world, 1.0 might actually be 1.0000000000000002, and your program just keeps spinning.

The naive way to handle this is to invent a "magic number" for your tolerance. You'll see a lot of code that looks like this:

if (fabs(a - b) < 0.00001) {
    // Treat them as equal
}

The fragility of hardcoded tolerances

This looks fine on the surface, but it's a ticking time bomb. The problem is that 0.00001 is an arbitrary choice. If you're calculating the distance between two galaxies in meters, 0.00001 is impossibly strict; your check will almost always fail. If you're calculating the thickness of a microscopic cell membrane, 0.00001 is massive, and your check will treat two completely different values as identical.

You're basically guessing where the precision of the hardware ends and the "noise" begins. I've seen this cause catastrophic failures in embedded systems where the code worked in the simulator but failed on the actual hardware because the precision characteristics of the target CPU were slightly different.

Tuning to the machine with float.h

Instead of guessing, we use <float.h>. This header tells us exactly what the hardware is capable of. The most important tool here is DBL_EPSILON (or FLT_EPSILON for floats). Epsilon represents the difference between 1.0 and the next representable value of that type. It is the smallest possible "step" the machine can take at that magnitude.

The better way to compare two numbers is to scale that epsilon based on the magnitude of the numbers you're comparing. It looks more like this:

#include <float.h>
#include <math.h>

int are_nearly_equal(double a, double b) {
    double diff = fabs(a - b);
    a = fabs(a);
    b = fabs(b);
    double largest = (b > a) ? b : a;

    if (diff <= largest * DBL_EPSILON) {
        return 1;
    }
    return 0;
}

By multiplying DBL_EPSILON by the larger of the two numbers, we create a sliding window of tolerance. As the numbers get larger, the tolerance grows; as they get smaller, it shrinks. We're no longer guessing; we're letting the C standard and the hardware define the limit of precision.

Avoiding the "Infinity" Trap

Beyond precision, there's the issue of limits. I often see people use 1e30 or some other massive number to represent "infinity" or a "maximum possible value" when initializing a variable for a minimum-search algorithm. The problem is that 1e30 might not actually be the maximum. If your data happens to exceed that, your logic breaks.

float.h provides FLT_MAX and DBL_MAX. These aren't just big numbers; they are the absolute ceiling of what the type can hold. Using DBL_MAX tells anyone reading your code, "I am using the absolute limit of this architecture." It also prevents bugs where you might accidentally overflow your "magic infinity" if you perform a calculation on it.

Conversely, FLT_MIN is a bit of a trip. It's not the most negative number (that would be -FLT_MAX); it's the smallest positive normalized value. If you're writing a routine to prevent division by zero, checking if a value is less than FLT_MIN is a much safer bet than checking if it's exactly 0.0, because it catches values that are so small they're effectively zero for any practical calculation.




📋 Practical Task

Build a Magnitude-Aware Range Validator

Write a program that simulates a sensor reading system. You need to implement a function called is_within_tolerance that takes two double values (the measured value and the target value) and returns 1 if they are "equal enough" and 0 otherwise.

Your implementation must satisfy these requirements:

  • Use DBL_EPSILON from <float.h> to calculate the tolerance.
  • The tolerance must be scaled by the magnitude of the inputs (the larger of the two absolute values).
  • Include a check at the start of your program to print the actual value of DBL_MAX and DBL_EPSILON to the console so you can see the hardware limits you are working with.
  • Test your function with two sets of data: one set of very large numbers (e.g., 1 trillion) and one set of very small numbers (e.g., 1e-12) to prove that your scaled epsilon works where a hardcoded 0.00001 would fail.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.