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

«Как проверить равенство двух чисел с плавающей запятой (float/double)?» — вопрос из категории C++ Core, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Прямое сравнение чисел с плавающей запятой через == ненадёжно из-за ошибок округления и особенностей представления по стандарту 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, который может помочь в некоторых сравнениях, но универсальной функции в стандарте пока нет.