I actually asked an (unfortunately unanswered
:() question about something similar here Generic and accurate floating point "equality". Not trying to plug that - just wanted to give some reference.
While writing that code, I was relying pretty heavily on this article by Bruce Dawson. I strongly encourage reading it (and the series it is a part of). That being said, I think your code is fundamentally flawed as a generic solution. It might work as a specifically tailored solution, but you should
- know a lot about its domain
- thoroughly consider the consequences of your implementation
- accept those consequences
If you try to use this as a generic solution it will likely always fail for certain classes of floating points, and can be very easily misused to fail for all floating point numbers.
While you shouldn't necessarily scrap your function, you should very clearly document the limitations of your implementation, and potentially document uses of the function if there is any ambiguity as to how accurate (or not) that comparison is likely to be.
- Using a threshold value doesn't always work very well, and requires the programmer to understand their domain and floating point.
What you're doing is basically
bool isEqual = fabs(f1 – f2) <= epsilon;. There is no one-size fits all value for
epsilon - the
float.h is only reasonable for values between 1 and 2 - it becomes much too large/small very quickly as the numbers get smaller/bigger.
- Using ULPs doesn't help much (I have a personal preference for them, but as you'll see there is little to no difference)
If the integer representations of two same-sign floats are subtracted then the absolute value of the result is equal to one plus the number of representable floats between them.
This might lead you (as it did me) into thinking that you can use those to more effectively compare if something is equal (or if they're close enough that you can call it floating point error). The problem is, once again, scaling - an ULP is a huge difference for large numbers, and a tiny one for small ones. Using ULPs (or any relative comparison) is pretty reasonable for "normal" numbers.
As an aside - the relationships between ULPs and a relative difference (what you're doing) is pretty neat -
Image credit: Bruce Dawson
- NaNs and infinities are weird, and there isn't a one-size fits all solution for them
Maybe you don't expect to ever find any NaNs or infinities in the code that uses this functor, or maybe you don't care. Either way, it should be documented that this may do something unexpected if a number is NaN/infinity. You could also choose how to handle this in your code - in many applications any kind of comparison with a NaN returns false, while you can generally do less than or greater compared to an infinity of the appropriate sign (if you're careful). Whatever you do, that choice should be documented.
- Zeroes and denormals will ruin your day
From Bruce Dawson:
It turns out that the entire idea of relative epsilons breaks down near zero. The reason is fairly straightforward. If you are expecting a result of zero then you are probably getting it by subtracting two numbers. In order to hit exactly zero the numbers you are subtracting need to be identical. If the numbers differ by one ULP then you will get an answer that is small compared to the numbers you are subtracting, but enormous compared to zero.
Small numbers break lots of things - hooray! Maybe this isn't an issue for your application - you deal with planets, not atoms - but it can really throw a wrench in your plans. In order to "reliably" compare to denormals and zero you need an absolute epsilon, not a relative one.
All in all - comparing floating point numbers generically and with confidence is hard. There are a few overall takeaways
- When comparing against zero any type of relative comparison (epsilon or ULP based) is useless. You need an absolute epsilon, and finding the right value depends on your domain and will take tinkering (and probably still be wrong in some cases).
- If you're comparing against non-zero (and more generally non-denormal) numbers then relative comparisons (either epsilon or ULP based) is fine. If there is a constant number being compared against then you could use an absolute epsilon.
- Be mindful of how you handle NaNs and infinities - what does it mean for your application to encounter such values, and what does it mean to compare against them? This can only be handled by applying information about your domain to them.
- From Bruce Dawson: > If you are comparing two arbitrary numbers that could be zero or non-zero then you need the kitchen sink. Good luck and God speed.
I don't have anything in particular to say about the code itself that wasn't mentioned by another answer, but I hope you consider the implications of floating point comparisons carefully.