Is a C++ Float Variable Ever Equal to 0.0f?

The answer to the question in the title is a resounding “maybe”. The writer of a piece of code wanted to avoid a division-by-zero error by checking whether the divisor of type float is not equal to 0.0f.

During a code review, I found the following piece of code.

// signal.h
struct Signal {
    int id;
    int offset;
    float factor;
    int value;
};

// builder.h
#include "signal.h"

class Builder {
    void updateSignalValue(int id, int value);
    QList<Signal> m_signalColl;
};

// builder.cpp
void Builder::updateSignalValue(int id, int rawValue) {
    for (auto& signal : m_signalColl) {
        if (signal.id == id) {
            rawValue -= signal.offset;
            if (signal.factor != 0.0f) {
                rawValue /= signal.factor;
            }
            signal.value = rawValue;
        }
    }
}

The function updateSignalValue() searches for a Signal with the identifier id in the list m_signalColl of Signal objects. If it finds one, it calculates the new value from the rawValue and stores the new value in the Signal object. The code writer wants to avoid a division-by-zero error with the condition signal.factor != 0.0f.

My first thought was whether this condition can ever be false. It turns out it can. If we simply assign 0.0f to signal.factor, the condition will always be false, because 0.0f is equal to 0.0f. The function updateSignalValue() would behave as follows.

    m_signalColl.push_back(Signal{1, 0, 0.0f, 5});

    updateSignalValue(1, 7);
    // m_signalColl[0].value == 7

    updateSignalValue(1, 4);
    // m_signalColl[0].value == 4

This works fine, because 0.0f has a unique representation as a float.

As soon as we calculate the value of signal.factor, we can introduce rounding errors with every arithmetic operation. We use the following function to calculate zero in a non-trivial way.

float Builder::calculateZero(float start, float decrement, int count)
{
    for (int i = 0; i < count; ++i) {
        start -= decrement;
    }
    return start;
}

Let us see how the results change when we use this function.

    m_signalColl.push_back(Signal{2, 0, calculateZero(1.0f, 0.2f, 5), 5});
    // m_signalColl[0].factor = 0.000000029802322

    updateSignalValue(2, 7);
    // m_signalColl[0].value == 234881024

    updateSignalValue(2, 4);
    // m_signalColl[0].value == 134217728

We see that the signal factor is nearly zero (0.000000029802322) but not exactly. The reason is that 0.2f has no exact representation as a binary floating-point number. It comes out as 0.200000002980232 when rounded to 15 digits after the decimal point. When calculateZero() subtracts 0.200000002980232 5 times from 1.0f, the result will be slightly less than 0.0f. The initial rounding error accumulates.

With this in mind, the following results are not too surprising, as 0.5f has an exact representation as a binary floating-point number. So, there are no rounding errors.

    m_signalColl.push_back(Signal{3, 0, calculateZero(2.5f, 0.5f, 5), 5});
    // m_signalColl[0].factor == 0.0f

    updateSignalValue(3, 7);
    // m_signalColl[3].value == 7

    updateSignalValue(2, 4);
    // m_signalColl[3].value == 4

One thing should be more than clear by now: The function updateSignalValue() does not work properly in general. It may work under certain assumptions, but it doesn’t state them.

That was the point when I tried to figure out the possible values of signal.factor. It was quickly clear that only positive integers (non-zero) were used. Therefore, the best remedy is to change the type of the field factor in the structure Signal from float into int. The non-zero check in updateSignalValue()

    if (signal.factor != 0) {

becomes meaningful and unambiguous. And, the function updateSignalValue() clearly states all of its assumptions. There is no need for anyone to figure them out.

This change has another positive effect. The division of rawValue by signal.factor becomes a true integer division. Before the change, it divided an integer by a float. This implied a couple of implicit conversions. rawValue and signal.factor were converted into doubles to be able to calculate the result of the division rawValue / signal.factor as a double. This double was finally converted into an int. That was a lot of needless conversions, which were a strong hint that signal.factor should always have been an integer.

So, the answer to the question whether a float variable is equal to 0.0f is: sometimes yes, sometimes no. Checking for equality or inequality with 0.0f is most often a not so pleasant smell that something is wrong with our code. So, we should analyse the problem and fix the root cause for the smell.