-1

I'm working on a Vec3 template class that (not surprisingly) stores a vector of three elements of any type. Here's the class header with the irrelevant bits stripped:

template <typename T>
class Vec3
{
private:
    T data[3] = {0};

public:
    inline Vec3 (const T & d0, const T & d1, const T & d2) noexcept;
    inline Vec3 (const Vec3 & other) noexcept;

    inline void operator =  (const Vec3 & other) noexcept;
    inline Vec3 operator -  (const Vec3 & other) noexcept;
    inline bool operator == (const Vec3 & other) const noexcept;
};

typedef Vec3<float> Vec3f;

template <typename T>
inline Vec3<T> :: Vec3 (const T & d0, const T & d1, const T & d2) noexcept
{
    data[0] = d0;
    data[1] = d1;
    data[2] = d2;
}

template <typename T>
inline Vec3<T> :: Vec3 (const Vec3<T> & other) noexcept
{
    *this = other;
}

template <typename T>
inline void Vec3<T> :: operator = (const Vec3<T> & other) noexcept
{
    data[0] = other.data[0];
    data[1] = other.data[1];
    data[2] = other.data[2];
}

template <typename T>
inline Vec3<T> Vec3<T> :: operator - (const Vec3<T> & other) noexcept
{
    return Vec3<T>(
        data[0] - other.data[0],
        data[1] - other.data[1],
        data[2] - other.data[2]
    );
}

template <typename T>
inline bool Vec3<T> :: operator == (const Vec3<T> & other) const noexcept
{
    if (std::is_same_v<T, double>)
    {
        return
            DoubleEq(data[0], other.data[0]) &&
            DoubleEq(data[1], other.data[1]) &&
            DoubleEq(data[2], other.data[2]);
    }
    else if (std::is_same_v<T, float>)
    {
        return
            FloatEq(data[0], other.data[0]) &&
            FloatEq(data[1], other.data[1]) &&
            FloatEq(data[2], other.data[2]);
    }
    else
    {
        return
            data[0] == other.data[0] &&
            data[1] == other.data[1] &&
            data[2] == other.data[2];
    }
}

The epsilon comparison functions are defined here:

inline bool DoubleEq (const double a, const double b)
{
    return (fabs(a - b) < std::numeric_limits<double>::epsilon());
}

inline bool FloatEq (const float a, const float b)
{
    return (fabs(a - b) < std::numeric_limits<float>::epsilon());
}

I'm testing it with the GoogleTest suite and am having trouble with the following snippet:

Vec3f vecf1 = {32.1432,  768.1343, 9789.1345};
Vec3f vecf2 = {643.5348, 75.4232,  20.4701};

Vec3f vecf3 = vecf1 - vecf2;

EXPECT_TRUE(vecf1 == Vec3f(32.1432,          768.1343,         9789.1345));         // Passes
EXPECT_TRUE(vecf2 == Vec3f(643.5348,         75.4232,          20.4701));           // Passes
EXPECT_TRUE(vecf3 == Vec3f(32.1432-643.5348, 768.1343-75.4232, 9789.1345-20.4701)); // Fails :(

This sould be straightforward because the numbers used in the third test to build the comparison vector are exactly the same as the ones in vecf1 and vecf2. However, printing vec3f and the raw operations yields different values:

std::cout << vecf3[0]         << " " << vecf3[1]         << " " << vecf3[2]          << std::endl;
std::cout << 32.1432-643.5348 << " " << 768.1343-75.4232 << " " << 9789.1345-20.4701 << std::endl;
-611.392 692.711 9768.67
-611.392 692.711 9768.66

What's more, I've tried debugging the program and have found that the values passed to Vec3<float>::operator-(Vec3<float> const&) differ from the ones written in the code. That's expected from floating point arithmetic loss of significance, but I don't know why this only happens then the floats are passed to a function:

119 inline Vec3<T> Vec3<T> :: operator - (const Vec3<T> & other) noexcept
(gdb) p this
$1 = (Vec3<float> * const) 0x5555555899a0
(gdb) p *this
$2 = {data = {32.1431999, 768.134277, 9789.13477}}
(gdb) p other
$3 = (const Vec3<float> &) @0x5555555899ac: {data = {643.53479, 75.4232025, 20.4701004}}
(gdb) n
124         data[2] - other.data[2]
(gdb) 
123         data[1] - other.data[1],
(gdb) 
122         data[0] - other.data[0],
(gdb) n
125     );
(gdb) n
126 }
(gdb) p data[0] - other.data[0]
$4 = -611.391602
(gdb) p data[1] - other.data[1]
$5 = 692.71106
(gdb) p data[2] - other.data[2]
$6 = 9768.66504
(gdb) n
Vec3Test_Subtraction_NoSideEffects_Test::TestBody (this=0x5555555898d0) at tests/vec3.hpp:218
218     std::cout << vecf3[0]         << " " << vecf3[1]         << " " << vecf3[2]          << std::endl;
(gdb) 
-611.392 692.711 9768.67
219     std::cout << 32.1432-643.5348 << " " << 768.1343-75.4232 << " " << 9789.1345-20.4701 << std::endl;
(gdb) 
-611.392 692.711 9768.66

As a sanity check, Valgrind returns no errors:

 1 FAILED TEST
==47278== 
==47278== HEAP SUMMARY:
==47278==     in use at exit: 0 bytes in 0 blocks
==47278==   total heap usage: 347 allocs, 347 frees, 138,501 bytes allocated
==47278== 
==47278== All heap blocks were freed -- no leaks are possible
==47278== 
==47278== For lists of detected and suppressed errors, rerun with: -s
==47278== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Why is my code evaluating floats differently in different situations? What am I doing wrong?

Groctel
  • 130
  • 11
  • 1
    You subtract `double` in one case and `float` in the other – chtz Feb 27 '21 at 12:25
  • @chtz Oooooh I see. TIL C++ defaults floating point numbers to doubles. It's working now. Please write a full answer and I'll accept it :D – Groctel Feb 27 '21 at 12:30
  • Also note that you can use `if constexpr`, in your comparison operator. – Mansoor Feb 27 '21 at 12:38
  • You may consider templating the functions that compare the difference between two values and epsilon, it might simplify the rest of the code (like `operator ==`). – Bob__ Feb 27 '21 at 12:39
  • I'm looking into it @M.A. Thanks! What do you mean by templating the comparison functions @Bob__? – Groctel Feb 27 '21 at 12:42
  • 1
    Look at how you have used `std::numeric_limits::epsilon()`, can you think of a way to write a single `template bool almost_equal(T a, T b)` instead of `DoubleEq` and `FloatEq`? – Bob__ Feb 27 '21 at 12:44
  • 2
    `inline bool Vec3 :: operator == (const Vec3 & other) const noexcept` this method, as nice as it is, is toxic. `==` should be transative, ie `a==b` and `b==c` implies `a==c`. The fact it is not means every `std` algorithm fed your vecs explodes if it uses `==`, for example. An "almost equal" is quite useful, but its name should not be `==` – Yakk - Adam Nevraumont Feb 27 '21 at 13:24
  • 1
    `fabs(a - b) < std::numeric_limits::epsilon()` is essentially useless. `epsilon()` is the difference between 1 and the next representable number. Numbers in [1, 2) can differ by `epsilon()` because the `double` format has precision to represent that difference, by definition of `epsilon()`. But the minimum difference of numbers in [2, 4) is twice that. They cannot differ by `epsilon()` because the format does not have the precision for that. For the sample values shown in the question, like `32.1432`, `fabs(a - b) < std::numeric_limits::epsilon()` is true only if `a == b`… – Eric Postpischil Feb 27 '21 at 14:26
  • 1
    Conversely, for very small numbers, `fabs(a - b) < std::numeric_limits::epsilon()` will incorrectly report that `a` and `b` are equal even if they are grossly different, such as +2^−100 and −23•2^−60. – Eric Postpischil Feb 27 '21 at 14:27
  • What should I use to compare floating point numbers then? – Groctel Feb 27 '21 at 14:49

1 Answers1

1

I see a couple of issues here:

Epsilon is a one bit error, but one bit at around 1. You should scale espilon to match the scale of your result.

The 1 bit error (1 epsilon) is good for 1 addition, the more complex the operation, the larger the bit error. So you may want to be able to specify it for re-use in other tests.

For example:

#include <cmath>

template <typename _Float> 
inline bool FloatEq (_Float a, _Float b, _Float bit_margin = _Float(1)) noexcept
{
    const _Float epsilon = std::max(std::abs(a), std::abs(b)) * std::numeric_limits<_Float>::epsilon();

    return std::abs(b - a) < (bit_margin * epsilon);
}
Michaël Roy
  • 6,338
  • 1
  • 15
  • 19
  • I understand how, but I don't understand why and can't find any references on why it should be done that way. Could you share a source or elaborate on the topic, please? – Groctel Feb 28 '21 at 17:57
  • 1
    Floatng point values are made of a mantissa and an exponent. Epsilon is 1 bit error in the matissa, but you must scale the exponent to match the value you are comparng against. Multiplying by the number you are matching against brings the exponents of the variables closer to each other. – Michaël Roy Feb 28 '21 at 18:08
  • I see. So the `_Float epsilon` is the corrected exponent and the bit margin the number of bit errors I am tolerating (that's why it's set to 1 by default), right? – Groctel Feb 28 '21 at 18:14
  • 1
    That's exactly it. – Michaël Roy Feb 28 '21 at 18:47