6.7 — Operatory relacyjne i porównania zmiennoprzecinkowe

Operatory relacyjne to operatory umożliwiające porównanie dwóch wartości. Istnieje 6 operatorów relacyjnych:

OperatorSymbolFormularzOperacja
Większy niż>x > yprawda, jeśli x jest większe niż y, fałsz w przeciwnym razie
Mniej niż<x < yprawda, jeśli x jest mniejsze niż y, fałsz w przeciwnym razie
Większy niż lub równa się>=x >= yprawda, jeśli x jest większe lub równe y, fałsz w przeciwnym razie
Mniejsze lub równe<=x <= yprawda, jeśli x jest mniejsze lub równe y, fałsz w przeciwnym razie
Równość==x == yprawda, jeśli x równa się y, fałsz w przeciwnym razie
Nierówność!=x != yprawda, jeśli x nie jest równe y, fałsz w przeciwnym razie

Widzieliście już, jak działa większość z nich i są one dość intuicyjne. Każdy z tych operatorów zwraca wartość logiczną prawda (1) lub fałsz (0).

Oto przykładowy kod wykorzystujący te operatory z liczbami całkowitymi:

#include <iostream>

int main()
{
    std::cout << "Enter an integer: ";
    int x{};
    std::cin >> x;

    std::cout << "Enter another integer: ";
    int y{};
    std::cin >> y;

    if (x == y)
        std::cout << x << " equals " << y << '\n';
    if (x != y)
        std::cout << x << " does not equal " << y << '\n';
    if (x > y)
        std::cout << x << " is greater than " << y << '\n';
    if (x < y)
        std::cout << x << " is less than " << y << '\n';
    if (x >= y)
        std::cout << x << " is greater than or equal to " << y << '\n';
    if (x <= y)
        std::cout << x << " is less than or equal to " << y << '\n';

    return 0;
}

A wyniki przykładowego przebiegu:

Enter an integer: 4
Enter another integer: 5
4 does not equal 5
4 is less than 5
4 is less than or equal to 5

Te operatory są niezwykle proste w użyciu przy porównywaniu liczb całkowitych.

Boolean wartości warunkowe

Domyślnie warunki instrukcji if lub operator warunkowy (i kilka innych miejsc) oceniane jako wartości logiczne.

Wielu nowych programistów napisze instrukcje takie jak to:

if (b1 == true) ...

Jest to zbędne, ponieważ == true w rzeczywistości nie dodaje żadnej wartości do warunku. Zamiast tego powinniśmy napisać:

if (b1) ...

Podobnie, poniższe:

if (b1 == false) ...

lepiej zapisać jako:

if (!b1) ...

Najlepsza praktyka

Nie dodawaj niepotrzebnych == lub != do warunków. Utrudnia to ich odczytanie bez podania dodatkowej wartości.

Porównanie obliczonych wartości zmiennoprzecinkowych może być problematyczne

Rozważ następujący program:

#include <iostream>

int main()
{
    constexpr double d1{ 100.0 - 99.99 }; // should equal 0.01 mathematically
    constexpr double d2{ 10.0 - 9.99 }; // should equal 0.01 mathematically

    if (d1 == d2)
        std::cout << "d1 == d2" << '\n';
    else if (d1 > d2)
        std::cout << "d1 > d2" << '\n';
    else if (d1 < d2)
        std::cout << "d1 < d2" << '\n';
    
    return 0;
}

Obydwie zmienne d1 i d2 powinny mieć wartość 0.01. Ale ten program wypisuje nieoczekiwany wynik:

d1 > d2

Jeśli sprawdzisz wartości d1 i d2 w debugerze, prawdopodobnie zobaczysz, że d1 = 0,010000000000005116 i d2 = 0,009999999999997868. Obie liczby są bliskie 0,01, ale d1 jest większe niż, a d2 mniejsze niż.

Porównywanie wartości zmiennoprzecinkowych przy użyciu dowolnego operatora relacyjnego może być niebezpieczne. Dzieje się tak dlatego, że wartości zmiennoprzecinkowe nie są dokładne, a małe błędy zaokrągleń w operandach zmiennoprzecinkowych mogą spowodować, że będą one nieco mniejsze lub nieco większe niż oczekiwano. A to może zniechęcić operatory relacyjne.

Powiązana treść

Omówiliśmy błędy zaokrąglania na lekcji 4.8 — Liczby zmiennoprzecinkowe.

Rzeczy zmiennoprzecinkowe mniejsze i większe niż

Gdy operatory mniejsze niż (<), większe niż (>), mniejsze niż równe (<=) i większe niż równe (>=) są używane z wartościami zmiennoprzecinkowymi, w większości przypadków dadzą one wiarygodną odpowiedź (kiedy wartość operandy nie są podobne). Jeśli jednak operandy są prawie identyczne, operatory te należy uznać za niewiarygodne. Na przykład d1 > d2 zdarza się, że w powyższym przykładzie generowany jest true , ale mógłby powstać false gdyby błędy numeryczne potoczyły się w innym kierunku.

Jeśli konsekwencja uzyskania błędnej odpowiedzi, gdy operandy są podobne, jest akceptowalna, wówczas użycie tych operatorów może być dopuszczalne. Jest to decyzja zależna od aplikacji.

Rozważmy na przykład grę (np. Space Invaders), w której chcesz ustalić, czy przecinają się dwa poruszające się obiekty (takie jak pocisk i kosmita). Jeśli obiekty są nadal daleko od siebie, operatory te zwrócą poprawną odpowiedź. Jeśli te dwa obiekty są bardzo blisko siebie, możesz uzyskać odpowiedź w obie strony. W takich przypadkach zła odpowiedź prawdopodobnie nawet nie zostałaby zauważona (wyglądałoby to na prawie chybienie lub prawie trafienie), a gra toczyłaby się dalej.

Równość i nierówność zmiennoprzecinkowa

Operatory równości (== i !=) są znacznie bardziej kłopotliwe. Rozważmy operator==, który zwraca wartość true tylko wtedy, gdy jego operandy są dokładnie równe. Ponieważ nawet najmniejszy błąd zaokrąglenia spowoduje, że dwie liczby zmiennoprzecinkowe nie będą równe, operator== jest narażony na duże ryzyko zwrócenia wartości false, gdy można się spodziewać wartości true. Operator!= ma ten sam problem.

#include <iostream>

int main()
{
    std::cout << std::boolalpha << (0.3 == 0.2 + 0.1); // prints false

    return 0;
}

Z tego powodu należy zasadniczo unikać używania tych operatorów z operandami zmiennoprzecinkowymi.

Ostrzeżenie

Unikaj używania operator== i operator!= do porównywania wartości zmiennoprzecinkowych, jeśli istnieje jakakolwiek szansa, że te wartości zostały obliczone.

Istnieje jeden godny uwagi wyjątek od powyższego: Można bezpiecznie porównać literał zmiennoprzecinkowy ze zmienną tego samego typu, która została zainicjowana literałem tego samego typu, o ile liczba cyfr znaczących w każdym literale nie przekracza minimalnej precyzji dla tego typu. Float ma minimalną precyzję wynoszącą 6 cyfr znaczących, a double ma minimalną precyzję wynoszącą 15 cyfr znaczących.

Dokładność dla różnych typów omówimy w lekcji 4.8 — Liczby zmiennoprzecinkowe.

Na przykład możesz czasami zobaczyć funkcję, która zwraca literał zmiennoprzecinkowy (zwykle 0.0, a czasami 1.0). W takich przypadkach można bezpiecznie przeprowadzić bezpośrednie porównanie z tą samą wartością literału tego samego typu:

if (someFcn() == 0.0) // okay if someFcn() returns 0.0 as a literal only
    // do something

Zamiast literału możemy także porównać zmienną zmiennoprzecinkową const lub constexpr, która została zainicjowana wartością literału:

constexpr double gravity { 9.8 };
if (gravity == 9.8) // okay if gravity was initialized with a literal
    // we're on earth

Porównywanie literałów zmiennoprzecinkowych różnych typów jest przeważnie niebezpieczne. Na przykład porównanie 9.8f Do 9.8 zwróci wartość false.

Wskazówka

Bezpieczne jest porównanie literału zmiennoprzecinkowego ze zmienną tego samego typu, która została zainicjowana literałem tego samego typu, o ile liczba cyfr znaczących w każdym literale nie przekracza minimalnej precyzji dla tego typu. Float ma minimalną precyzję 6 cyfr znaczących, a double ma minimalną precyzję 15 cyfr znaczących.

Porównywanie literałów zmiennoprzecinkowych różnych typów jest generalnie niebezpieczne.

Porównywanie liczb zmiennoprzecinkowych (odczyt zaawansowany/opcjonalnie)

Jak więc możemy rozsądnie porównać dwa operandy zmiennoprzecinkowe, aby sprawdzić, czy są równe?

Najpopularniejsza metoda wykonywanie równości zmiennoprzecinkowej wymaga użycia funkcji, która sprawdza, czy dwie liczby są prawie takie same. Jeśli są „wystarczająco blisko”, nazywamy je równymi. Wartość używana do reprezentowania „wystarczająco blisko” jest tradycyjnie nazywana epsilon. Epsilon jest ogólnie definiowany jako mała liczba dodatnia (np. 0,00000001, czasami zapisywana jako 1e-8).

Nowi programiści często próbują napisać własną „wystarczająco bliską” funkcję w następujący sposób:

#include <cmath> // for std::abs()

// absEpsilon is an absolute value
bool approximatelyEqualAbs(double a, double b, double absEpsilon)
{
    // if the distance between a and b is less than or equal to absEpsilon, then a and b are "close enough"
    return std::abs(a - b) <= absEpsilon;
}

std::abs() to funkcja w nagłówku <cmath>, która zwraca wartość bezwzględną swojego argumentu. Zatem std::abs(a - b) <= absEpsilon sprawdza, czy odległość pomiędzy a i b jest mniejsza lub równa dowolnej wartości epsilon reprezentującej „wystarczająco blisko”. Jeśli a i b są wystarczająco blisko, funkcja zwraca wartość true, aby wskazać, że są równe. W przeciwnym razie zwraca wartość false.

Chociaż ta funkcja może działać, nie jest świetna. Epsilon 0.00001 jest dobry dla wejść w okolicach 1.0, jest za duży dla wejść w okolicach 0.0000001 i za mały dla wejść w rodzaju 10,000.

Na marginesie…

Jeśli powiemy, że dowolną liczbę mieszczącą się w zakresie 0,00001 od innej liczby należy traktować jako tę samą liczbę, to:

  • 1 i 1,0001 będą różne, ale 1 i 1,00001 byłoby takie samo. Nie jest to nierozsądne.
  • 0,0000001 i 0,00001 byłoby takie samo. Nie wydaje się to dobre, ponieważ te liczby różnią się o dwa rzędy wielkości.
  • 10000 i 10000,0001 byłoby inne. To również nie wydaje się dobre, ponieważ liczby te prawie się nie różnią, biorąc pod uwagę wielkość liczby.

Oznacza to, że za każdym razem, gdy wywołujemy tę funkcję, musimy wybrać epsilon odpowiedni dla naszych danych wejściowych. Jeśli wiemy, że będziemy musieli skalować epsilon proporcjonalnie do wielkości naszych danych wejściowych, równie dobrze możemy zmodyfikować tę funkcję, aby to za nas zrobiła.

Donald Knuth, słynny informatyk, zasugerował następującą metodę w swojej książce „The Art of Computer Programming, Volume II: Seminumerical Algorithms” (Addison-Wesley, 1969)”:

#include <algorithm> // for std::max
#include <cmath>     // for std::abs

// Return true if the difference between a and b is within epsilon percent of the larger of a and b
bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
	return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}

W tym przypadku zamiast epsilon jest liczbą bezwzględną, epsilon jest teraz zależny od wielkości a lub b.

Przyjrzyjmy się bardziej szczegółowo, jak działa ta szalenie wyglądająca funkcja. Po lewej stronie operatora <= std::abs(a - b) mówi nam odległość pomiędzy a i b jako liczbę dodatnią.

Po prawej stronie operatora <= musimy obliczyć największą wartość „wystarczająco blisko”, którą jesteśmy skłonni zaakceptować. Aby to zrobić, algorytm wybiera większy z a i b (jako przybliżony wskaźnik całkowitej wielkości liczb), a następnie mnoży go przez relEpsilon. W tej funkcji relEpsilon reprezentuje wartość procentową. Na przykład, jeśli chcemy powiedzieć, że „wystarczająco blisko” oznacza, że a i b znajdują się w granicach 1% większego z a i b, przekazujemy relEpsilon wynoszący 0,01 (1% = 1/100 = 0,01). Wartość relEpsilon można dostosować do najbardziej odpowiedniej dla danych okoliczności (np. epsilon wynoszący 0,002 oznacza w granicach 0,2%).

Aby wykonać nierówność (!=) zamiast równości, po prostu wywołaj tę funkcję i użyj operatora logicznego NOT (!), aby odwrócić wynik:

if (!approximatelyEqualRel(a, b, 0.001))
    std::cout << a << " is not equal to " << b << '\n';

Zauważ, że chociaż funkcja okołoEqualRel() będzie działać w większości przypadków, tak nie jest idealnie, zwłaszcza gdy liczby zbliżają się do zera:

#include <algorithm> // for std::max
#include <cmath>     // for std::abs
#include <iostream>

// Return true if the difference between a and b is within epsilon percent of the larger of a and b
bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
	return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}

int main()
{
    // a is really close to 1.0, but has rounding errors
    constexpr double a{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 };

    constexpr double relEps { 1e-8 };
    constexpr double absEps { 1e-12 };

    std::cout << std::boolalpha; // print true or false instead of 1 or 0
    
    // First, let's compare a (almost 1.0) to 1.0.
    std::cout << approximatelyEqualRel(a, 1.0, relEps) << '\n';
 
    // Second, let's compare a-1.0 (almost 0.0) to 0.0
    std::cout << approximatelyEqualRel(a-1.0, 0.0, relEps) << '\n';

    return 0;
}

Być może zaskakujące jest to, że zwraca:

true
false

Drugie połączenie nie zadziałało zgodnie z oczekiwaniami. Matematyka po prostu załamuje się blisko zera.

Jednym ze sposobów uniknięcia tego jest użycie zarówno epsilon absolutnego (jak zrobiliśmy w pierwszym podejściu), jak i epsilon względny (jak zrobiliśmy w podejściu Knutha):

// Return true if the difference between a and b is less than or equal to absEpsilon, or within relEpsilon percent of the larger of a and b
bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
    // Check if the numbers are really close -- needed when comparing numbers near zero.
    if (std::abs(a - b) <= absEpsilon)
        return true;

    // Otherwise fall back to Knuth's algorithm
    return approximatelyEqualRel(a, b, relEpsilon);
}

W tym algorytmie najpierw sprawdzamy, czy a i b są blisko siebie w wartościach bezwzględnych, co obsługuje przypadek, gdy oba a i b są blisko do zera. Parametr absEpsilon należy ustawić na bardzo małą wartość (np. 1e-12). Jeśli to się nie powiedzie, wracamy do algorytmu Knutha, używając względnego epsilon.

Oto nasz poprzedni kod testujący oba algorytmy:

#include <algorithm> // for std::max
#include <cmath>     // for std::abs
#include <iostream>

// Return true if the difference between a and b is within epsilon percent of the larger of a and b
bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
	return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}

// Return true if the difference between a and b is less than or equal to absEpsilon, or within relEpsilon percent of the larger of a and b
bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
    // Check if the numbers are really close -- needed when comparing numbers near zero.
    if (std::abs(a - b) <= absEpsilon)
        return true;

    // Otherwise fall back to Knuth's algorithm
    return approximatelyEqualRel(a, b, relEpsilon);
}

int main()
{
    // a is really close to 1.0, but has rounding errors
    constexpr double a{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 };

    constexpr double relEps { 1e-8 };
    constexpr double absEps { 1e-12 };

    std::cout << std::boolalpha; // print true or false instead of 1 or 0

    std::cout << approximatelyEqualRel(a, 1.0, relEps) << '\n';     // compare "almost 1.0" to 1.0
    std::cout << approximatelyEqualRel(a-1.0, 0.0, relEps) << '\n'; // compare "almost 0.0" to 0.0

    std::cout << approximatelyEqualAbsRel(a, 1.0, absEps, relEps) << '\n';     // compare "almost 1.0" to 1.0
    std::cout << approximatelyEqualAbsRel(a-1.0, 0.0, absEps, relEps) << '\n'; // compare "almost 0.0" to 0.0

    return 0;
}
true
false
true
true

Widzisz, że w przybliżeniuEqualAbsRel() poprawnie obsługuje małe dane wejściowe.

Porównanie liczb zmiennoprzecinkowych to trudny temat i nie ma jednego algorytmu, który pasowałby do wszystkich, który działałby w każdym przypadku. Jednakże funkcja aboutEqualAbsRel() z absEpsilon 1e-12 i relEpsilon 1e-8 powinna wystarczyć do obsługi większości przypadków, z którymi się spotkasz.

Tworzenie approximatelyEqual funkcji constexpr Zaawansowane

W C++23 obie approximatelyEqual funkcje można utworzyć constexpr przez dodanie the constexpr słowem kluczowym:

// C++23 version
#include <algorithm> // for std::max
#include <cmath>     // for std::abs (constexpr in C++23)

// Return true if the difference between a and b is within epsilon percent of the larger of a and b
constexpr bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
	return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}

// Return true if the difference between a and b is less than or equal to absEpsilon, or within relEpsilon percent of the larger of a and b
constexpr bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
    // Check if the numbers are really close -- needed when comparing numbers near zero.
    if (std::abs(a - b) <= absEpsilon)
        return true;

    // Otherwise fall back to Knuth's algorithm
    return approximatelyEqualRel(a, b, relEpsilon);
}

Powiązana treść

Funkcje constexpr omawiamy na lekcji F.1 -- Funkcje Constexpr.

Jednak przed wersją C++ 23 napotkaliśmy problem. Jeśli te funkcje constexpr zostaną wywołane w wyrażeniu stałym, nie powiedzie się:

int main()
{
    // a is really close to 1.0, but has rounding errors
    constexpr double a{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 };

    constexpr double relEps { 1e-8 };
    constexpr double absEps { 1e-12 };

    std::cout << std::boolalpha; // print true or false instead of 1 or 0

    constexpr bool same { approximatelyEqualAbsRel(a, 1.0, absEps, relEps) }; // compile error: must be initialized by a constant expression
    std::cout << same << '\n';

    return 0;
}

Dzieje się tak, ponieważ funkcja constexpr używana w wyrażeniu stałym nie może wywołać funkcji innej niż constexpr i std::abs nie została utworzona jako constexpr aż do C++ 23.

Łatwo to jednak naprawić — możemy po prostu porzucić std::abs na rzecz naszego własnego absolutu constexpr implementacja wartości.

// C++14/17/20 version
#include <algorithm> // for std::max
#include <iostream>

// Our own constexpr implementation of std::abs (for use in C++14/17/20)
// In C++23, use std::abs
// constAbs() can be called like a normal function, but can handle different types of values (e.g. int, double, etc...)
template <typename T>
constexpr T constAbs(T x)
{
    return (x < 0 ? -x : x);
}

// Return true if the difference between a and b is within epsilon percent of the larger of a and b
constexpr bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
    return (constAbs(a - b) <= (std::max(constAbs(a), constAbs(b)) * relEpsilon));
}

// Return true if the difference between a and b is less than or equal to absEpsilon, or within relEpsilon percent of the larger of a and b
constexpr bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
    // Check if the numbers are really close -- needed when comparing numbers near zero.
    if (constAbs(a - b) <= absEpsilon)
        return true;

    // Otherwise fall back to Knuth's algorithm
    return approximatelyEqualRel(a, b, relEpsilon);
}

int main()
{
    // a is really close to 1.0, but has rounding errors
    constexpr double a{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 };

    constexpr double relEps { 1e-8 };
    constexpr double absEps { 1e-12 };

    std::cout << std::boolalpha; // print true or false instead of 1 or 0

    constexpr bool same { approximatelyEqualAbsRel(a, 1.0, absEps, relEps) };
    std::cout << same << '\n';

    return 0;
}

Dla zaawansowanych czytelników

Wersja constAbs() powyższa to szablon funkcji, który pozwala na napisanie jednej definicji, która może obsługiwać różne typy wartości. Szablony funkcji omówimy w lekcji 11.6 -- Szablony funkcji.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
345 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze