Как проверить равенство двух чисел с плавающей запятой (float/double)?

Ответ

Прямое сравнение чисел с плавающей запятой через == ненадёжно из-за ошибок округления и особенностей представления по стандарту IEEE 754. Необходимо сравнивать с учётом допустимой погрешности (эпсилон).

Рекомендуемый подход — сравнение с относительной погрешностью:

#include <cmath>
#include <algorithm>
#include <limits>

bool approximatelyEqual(double a, double b, double epsilon = 1e-8) {
    // Сравниваем абсолютную разницу для чисел, близких к нулю
    double absDiff = std::fabs(a - b);
    if (absDiff <= epsilon) {
        return true;
    }
    // Используем относительную погрешность для остальных случаев
    double maxVal = std::max(std::fabs(a), std::fabs(b));
    return absDiff <= epsilon * maxVal;
}

Более надёжная функция, учитывающая краевые случаи:

bool nearlyEqual(double a, double b, double absEpsilon = 1e-12, double relEpsilon = 1e-8) {
    double diff = std::fabs(a - b);
    if (diff <= absEpsilon) {
        return true; // Числа практически идентичны или оба очень близки к нулю
    }
    // Используем относительную погрешность, основанную на большем из чисел
    double larger = std::max(std::fabs(a), std::fabs(b));
    return diff <= larger * relEpsilon;
}

// Пример использования
int main() {
    double x = 0.1 + 0.2;
    double y = 0.3;

    if (nearlyEqual(x, y)) {
        std::cout << "Numbers are considered equal.n";
    }
    // Сравнение с нулём требует особого подхода
    double z = 1e-20;
    if (std::fabs(z) < 1e-15) { // Используем абсолютную погрешность
        std::cout << "z is effectively zero.n";
    }
    return 0;
}

Ключевые моменты:

  • Абсолютный эпсилон (absEpsilon) полезен для сравнения чисел, близких к нулю.
  • Относительный эпсилон (relEpsilon) масштабируется с величиной чисел, что делает его универсальным.
  • Не используйте фиксированный эпсилон (например, 1e-5) для всех диапазонов чисел.
  • Для специальных значений (NaN, Inf) используйте std::isnan() и std::isinf().
  • В C++20 появился std::lerp, который может помочь в некоторых сравнениях, но универсальной функции в стандарте пока нет.

Ответ 18+ 🔞

О, ну ты держись, сейчас про плавающую точку расскажу так, что сам от себя охуеешь. Смотри, чувак, ты когда пишешь if (0.1 + 0.2 == 0.3), ты думаешь — ну логично же, ёпта! А нихуя! Компьютер тебе в ответ: false, пидарас шерстяной. И волнение ебать начинается: "Как так-то? Я же в школе учился!".

А всё потому, что числа с плавающей запятой — это пиздопроебибна история. Они в памяти хранятся как двоичные дроби, и банальная 0.1 в двоичном виде — это бесконечная дробь, типа нашей 1/3 в десятичной. Компьютер её обрезает, округляет, и получается мелкая, но ошибка. Складываешь — ошибка копится. И вот уже 0.1 + 0.2 — это не 0.3, а 0.30000000000000004. И твой оператор ==, тупой как пробка, смотрит на эти биты и говорит: "Не, братан, разные числа, иди на хуй". Доверия к такому сравнению — ноль ебать.

Так что делать? Нельзя сравнивать напрямую через ==, это путь в никуда, прям вротберунчик. Нужно сравнивать с допуском. То есть говорить: "Если числа отличаются на совсем чуть-чуть, будем считать их одинаковыми".

Вот, смотри, рабочий вариант:

bool approximatelyEqual(double a, double b, double epsilon = 1e-8) {
    double absDiff = std::fabs(a - b);
    if (absDiff <= epsilon) {
        return true;
    }
    double maxVal = std::max(std::fabs(a), std::fabs(b));
    return absDiff <= epsilon * maxVal;
}

Но это ещё цветочки. Потому что если числа оба близки к нулю, то относительная погрешность — хуй с горы. Нужен более хитрожопый подход, который учитывает и абсолютную разницу для мелких чисел, и относительную — для крупных.

bool nearlyEqual(double a, double b, double absEpsilon = 1e-12, double relEpsilon = 1e-8) {
    double diff = std::fabs(a - b);
    if (diff <= absEpsilon) {
        return true; // Ну почти ноль, да похуй
    }
    double larger = std::max(std::fabs(a), std::fabs(b));
    return diff <= larger * relEpsilon;
}

Вот это уже серьёзно. absEpsilon ловит случаи, когда оба числа — просто пыль, а relEpsilon работает для всего остального. Пример, чтобы ты не бздел:

int main() {
    double x = 0.1 + 0.2;
    double y = 0.3;

    if (nearlyEqual(x, y)) {
        std::cout << "Numbers are considered equal.n"; // Вот теперь сработает!
    }

    double z = 1e-20; // Это вообще пиздец как мало
    if (std::fabs(z) < 1e-15) { // Тут уже чисто на абсолютную погрешность смотрим
        std::cout << "z is effectively zero.n";
    }
    return 0;
}

Запомни раз и навсегда, ядрёна вошь:

  • Фиксированный epsilon (типа 0.0001) — манда с ушами. Для числа 1000000.0001 и 1000000.0002 он сработает, а для 0.00011 и 0.00012 — уже нет. И наоборот.
  • Сравнивать с нулём — отдельная песня. Только через абсолютную погрешность (std::fabs(z) < 1e-15).
  • NaN и бесконечности (Inf) — это вообще отдельный цирк. Для них есть std::isnan() и std::isinf(), == с ними — это вы ходите по охуенно тонкому льду.

В общем, чувак, мораль проста: никогда не используй == или != для double и float. Никогда. Это прямой билет в мир плавающих багов, которые ты будешь искать неделями. Бери готовую функцию nearlyEqual и живи спокойно. Э бошка думай!