Sie müssen diese Verarbeitung für Fließkomma-Vergleiche durchführen, da Fließkomma-Typen nicht so perfekt verglichen werden können wie Ganzzahl-Typen. Hier finden Sie Funktionen für die verschiedenen Vergleichsoperatoren.
Fließkomma gleich ( ==
)
Ich bevorzuge auch die Subtraktionstechnik, anstatt mich auf fabs()
o abs()
aber ich müsste ein Geschwindigkeitsprofil auf verschiedenen Architekturen erstellen, vom 64-Bit-PC bis zum ATMega328-Mikrocontroller (Arduino), um wirklich zu sehen, ob es einen großen Leistungsunterschied gibt.
Vergessen wir also den ganzen Kram mit den absoluten Werten und machen wir einfach ein paar Subtraktionen und Vergleiche!
Geändert von Das Beispiel von Microsoft hier :
/// @brief See if two floating point numbers are approximately equal.
/// @param[in] a number 1
/// @param[in] b number 2
/// @param[in] epsilon A small value such that if the difference between the two numbers is
/// smaller than this they can safely be considered to be equal.
/// @return true if the two numbers are approximately equal, and false otherwise
bool is_float_eq(float a, float b, float epsilon) {
return ((a - b) < epsilon) && ((b - a) < epsilon);
}
bool is_double_eq(double a, double b, double epsilon) {
return ((a - b) < epsilon) && ((b - a) < epsilon);
}
Beispiel für die Verwendung:
constexpr float EPSILON = 0.0001; // 1e-4
is_float_eq(1.0001, 0.99998, EPSILON);
Ich bin mir nicht ganz sicher, aber es scheint mir, dass einige der Kritikpunkte am epsilonbasierten Ansatz, wie sie in den folgenden Kommentaren beschrieben werden diese hoch bewertete Antwort kann durch die Verwendung einer Variablen epsilon gelöst werden, die entsprechend den zu vergleichenden Fließkommazahlen skaliert wird, etwa so:
float a = 1.0001;
float b = 0.99998;
float epsilon = std::max(std::fabs(a), std::fabs(b)) * 1e-4;
is_float_eq(a, b, epsilon);
Auf diese Weise skaliert der Epsilon-Wert mit den Fließkommawerten und ist daher nie so klein, dass er unbedeutend wird.
Der Vollständigkeit halber fügen wir noch den Rest hinzu:
Größer als ( >
), und weniger als ( <
):
/// @brief See if floating point number `a` is > `b`
/// @param[in] a number 1
/// @param[in] b number 2
/// @param[in] epsilon a small value such that if `a` is > `b` by this amount, `a` is considered
/// to be definitively > `b`
/// @return true if `a` is definitively > `b`, and false otherwise
bool is_float_gt(float a, float b, float epsilon) {
return a > b + epsilon;
}
bool is_double_gt(double a, double b, double epsilon) {
return a > b + epsilon;
}
/// @brief See if floating point number `a` is < `b`
/// @param[in] a number 1
/// @param[in] b number 2
/// @param[in] epsilon a small value such that if `a` is < `b` by this amount, `a` is considered
/// to be definitively < `b`
/// @return true if `a` is definitively < `b`, and false otherwise
bool is_float_lt(float a, float b, float epsilon) {
return a < b - epsilon;
}
bool is_double_lt(double a, double b, double epsilon) {
return a < b - epsilon;
}
Größer als oder gleich ( >=
), und kleiner oder gleich ( <=
)
/// @brief Returns true if `a` is definitively >= `b`, and false otherwise
bool is_float_ge(float a, float b, float epsilon) {
return a > b - epsilon;
}
bool is_double_ge(double a, double b, double epsilon) {
return a > b - epsilon;
}
/// @brief Returns true if `a` is definitively <= `b`, and false otherwise
bool is_float_le(float a, float b, float epsilon) {
return a < b + epsilon;
}
bool is_double_le(double a, double b, double epsilon) {
return a < b + epsilon;
}
Zusätzliche Verbesserungen:
- Ein guter Standardwert für
epsilon
in C++ ist std::numeric_limits<T>::epsilon()
, die entweder zu 0
o FLT_EPSILON
, DBL_EPSILON
o LDBL_EPSILON
. Siehe hier: https://en.cppreference.com/w/cpp/types/numeric_limits/epsilon . Sie können auch die float.h
Kopfzeile für FLT_EPSILON
, DBL_EPSILON
y LDBL_EPSILON
.
- Siehe https://en.cppreference.com/w/cpp/header/cfloat und
- https://www.cplusplus.com/reference/cfloat/
- Sie könnten die Funktionen stattdessen als Vorlage verwenden, um alle Fließkommatypen zu behandeln:
float
, double
y long double
, mit Typenprüfungen für diese Typen über eine static_assert()
innerhalb der Vorlage.
- Skalierung der
epsilon
Wert ist eine gute Idee, um sicherzustellen, dass es für wirklich große und wirklich kleine a
y b
Werte. Dieser Artikel empfiehlt und erklärt sie: http://realtimecollisiondetection.net/blog/?p=89 . Sie sollten also epsilon mit einem Skalierungswert skalieren, der gleich max(1.0, abs(a), abs(b))
wie in diesem Artikel erläutert. Andernfalls, wie a
und/oder b
zunehmen, würde das Epsilon im Verhältnis zu diesen Werten schließlich so klein werden, dass es im Fließkommafehler untergeht. Daher skalieren wir es so, dass es größer wird, wie die Werte selbst. Die Verwendung von 1.0
als kleinster zulässiger Skalierungsfaktor für epsilon gewährleistet auch, dass für wirklich kleine Größenordnungen a
y b
Werten wird epsilon selbst nicht so klein skaliert, dass es ebenfalls im Fließkommafehler verloren geht. Daher begrenzen wir den minimalen Skalierungsfaktor auf 1.0
.
- Wenn Sie die oben genannten Funktionen in einer Klasse "kapseln" wollen, lassen Sie es. Packen Sie sie stattdessen in einen Namespace ein, wenn Sie das möchten, um sie zu benennen. Beispiel: Wenn Sie alle eigenständigen Funktionen in einen Namespace namens
float_comparison
dann können Sie auf die is_eq()
Funktion, zum Beispiel so: float_comparison::is_eq(1.0, 1.5);
.
- Es wäre auch schön, Vergleiche gegen Null hinzuzufügen, nicht nur Vergleiche zwischen zwei Werten.
-
Hier ist also eine bessere Lösung mit den oben genannten Verbesserungen:
namespace float_comparison {
/// Scale the epsilon value to become large for large-magnitude a or b,
/// but no smaller than 1.0, per the explanation above, to ensure that
/// epsilon doesn't ever fall out in floating point error as a and/or b
/// increase in magnitude.
template<typename T>
static constexpr T scale_epsilon(T a, T b, T epsilon =
std::numeric_limits<T>::epsilon()) noexcept
{
static_assert(std::is_floating_point_v<T>, "Floating point comparisons "
"require type float, double, or long double.");
T scaling_factor;
// Special case for when a or b is infinity
if (std::isinf(a) || std::isinf(b))
{
scaling_factor = 0;
}
else
{
scaling_factor = std::max({(T)1.0, std::abs(a), std::abs(b)});
}
T epsilon_scaled = scaling_factor * std::abs(epsilon);
return epsilon_scaled;
}
// Compare two values
/// Equal: returns true if a is approximately == b, and false otherwise
template<typename T>
static constexpr bool is_eq(T a, T b, T epsilon =
std::numeric_limits<T>::epsilon()) noexcept
{
static_assert(std::is_floating_point_v<T>, "Floating point comparisons "
"require type float, double, or long double.");
// test `a == b` first to see if both a and b are either infinity
// or -infinity
return a == b || std::abs(a - b) <= scale_epsilon(a, b, epsilon);
}
/*
etc. etc.:
is_eq()
is_ne()
is_lt()
is_le()
is_gt()
is_ge()
*/
// Compare against zero
/// Equal: returns true if a is approximately == 0, and false otherwise
template<typename T>
static constexpr bool is_eq_zero(T a, T epsilon =
std::numeric_limits<T>::epsilon()) noexcept
{
static_assert(std::is_floating_point_v<T>, "Floating point comparisons "
"require type float, double, or long double.");
return is_eq(a, (T)0.0, epsilon);
}
/*
etc. etc.:
is_eq_zero()
is_ne_zero()
is_lt_zero()
is_le_zero()
is_gt_zero()
is_ge_zero()
*/
} // namespace float_comparison
Siehe auch:
- Die Makroformen einiger der oben genannten Funktionen in meinem Repo hier: utilities.h .
- UPDATE 29 NOV 2020: Es ist ein Work-in-Progress, und ich werde es eine separate Antwort machen, wenn es fertig ist, aber ich habe eine bessere, skalierte Epsilon-Version aller Funktionen in C in dieser Datei hier erstellt: utilities.c . Werfen Sie einen Blick darauf.
- WEITERE ABLESUNG I
zu tun haben jetzt getan haben: Fließkommatoleranzen neu betrachtet, von Christer Ericson . SEHR NÜTZLICHER ARTIKEL! Es spricht über die Skalierung epsilon, um sicherzustellen, dass es nie fällt in Fließkomma-Fehler, auch für wirklich große-Magnitude a
und/oder b
Werte!