← Назад к вопросам

Как проверишь равенство двух float?

2.3 Middle🔥 61 комментариев
#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как проверить равенство двух float?

Это критически важный вопрос при работе с вещественными числами. Прямое сравнение a == b может дать неправильный результат из-за ошибок округления.

Проблема: Ошибки округления

float a = 0.1 + 0.2;       // Может быть 0.3000000001
float b = 0.3;             // Может быть 0.3000000000

if (a == b) {
    std::cout << "Equal" << std::endl;  // Может не выполниться!
} else {
    std::cout << "Not equal" << std::endl;  // Скорее всего сюда
}

Почему так происходит?

Вещественные числа хранятся в двоичной форме (IEEE 754), и не все десятичные числа можно точно представить:

#include <iomanip>

std::cout << std::fixed << std::setprecision(20);
std::cout << 0.1f << std::endl;  // 0.10000000149011611938
std::cout << 0.2f << std::endl;  // 0.20000000298023223877
std::cout << 0.3f << std::endl;  // 0.29999999701976776123
std::cout << (0.1f + 0.2f) << std::endl;  // 0.30000001192092895508

Правильный способ 1: Epsilon сравнение (абсолютная ошибка)

const float EPSILON = 1e-6f;  // Выбирается в зависимости от точности

bool float_equal_absolute(float a, float b) {
    return std::abs(a - b) < EPSILON;
}

int main() {
    float x = 0.1f + 0.2f;
    float y = 0.3f;
    
    if (float_equal_absolute(x, y)) {
        std::cout << "Equal (with tolerance)" << std::endl;  // OK!
    }
}

Преимущества:

  • Просто и понятно
  • Хорошо для чисел близких к нулю

Недостатки:

  • EPSILON выбирается хардкодом
  • Не масштабируется для больших/малых чисел
  • Можно пропустить равные или ложно признать равными далёкие

Правильный способ 2: Relative epsilon (относительная ошибка)

bool float_equal_relative(float a, float b, float tolerance = 1e-6f) {
    float max_val = std::max(std::abs(a), std::abs(b));
    return std::abs(a - b) <= tolerance * max_val;
}

int main() {
    float x = 0.1f + 0.2f;
    float y = 0.3f;
    
    if (float_equal_relative(x, y)) {
        std::cout << "Equal!" << std::endl;
    }
}

Преимущества:

  • Масштабируется для больших и малых чисел
  • Процентный допуск вместо абсолютного

Недостатки:

  • Более сложная реализация
  • Проблемы когда оба числа близки к нулю

Правильный способ 3: Комбинированный подход (рекомендуемый)

bool float_equal(float a, float b, 
                float abs_tolerance = 1e-8f,
                float rel_tolerance = 1e-5f) {
    // Если числа очень близки к нулю, используй абсолютный допуск
    if (std::abs(a - b) < abs_tolerance) {
        return true;
    }
    
    // Для больших чисел используй относительный допуск
    float max_val = std::max(std::abs(a), std::abs(b));
    return std::abs(a - b) <= rel_tolerance * max_val;
}

int main() {
    std::cout << std::boolalpha;
    std::cout << float_equal(0.0f, 1e-9f) << std::endl;        // true
    std::cout << float_equal(1e6f, 1e6f + 0.1f) << std::endl;  // true
    std::cout << float_equal(0.1f + 0.2f, 0.3f) << std::endl;  // true
}

Способ 4: ULP (Units in the Last Place)

#include <cstring>
#include <limits>

bool float_equal_ulp(float a, float b, int max_ulps = 4) {
    // Интерпретируем float как int (битовое представление)
    int ia, ib;
    std::memcpy(&ia, &a, sizeof(float));
    std::memcpy(&ib, &b, sizeof(float));
    
    // Обрабатываем знак и специальные случаи
    if ((ia ^ ib) & 0x80000000) {
        return a == b;  // Разные знаки
    }
    
    // Вычисляем разницу в ULP
    int diff = std::abs(ia - ib);
    return diff <= max_ulps;
}

int main() {
    std::cout << float_equal_ulp(0.1f + 0.2f, 0.3f) << std::endl;  // true
}

Преимущества:

  • Точное сравнение на машинном уровне
  • Учитывает IEEE 754 представление

Недостатки:

  • Сложная реализация
  • Зависит от платформы

Сравнение с 0

// Для сравнения с нулём
bool is_zero(float x) {
    return std::abs(x) < 1e-6f;  // Абсолютный допуск
}

int main() {
    float x = std::sin(0.0f);  // Может быть 1e-9, не совсем 0
    
    if (is_zero(x)) {
        std::cout << "x is zero" << std::endl;
    }
}

Double vs Float

// Для double обычно выбирают другие допуски
const double EPSILON_D = 1e-15;  // Меньше из-за большей точности

bool double_equal(double a, double b) {
    return std::abs(a - b) < EPSILON_D;
}

Production функция

#include <cmath>
#include <limits>

template<typename T>
bool are_equal(T a, T b, 
              T abs_tol = std::numeric_limits<T>::epsilon(),
              T rel_tol = std::numeric_limits<T>::epsilon()) {
    // Проверка NaN
    if (std::isnan(a) || std::isnan(b)) {
        return std::isnan(a) && std::isnan(b);
    }
    
    // Проверка бесконечности
    if (std::isinf(a) || std::isinf(b)) {
        return a == b;
    }
    
    // Обычное сравнение
    T diff = std::abs(a - b);
    T max_abs = std::max(std::abs(a), std::abs(b));
    
    return diff < abs_tol || diff < rel_tol * max_abs;
}

int main() {
    std::cout << std::boolalpha;
    std::cout << are_equal(0.1f + 0.2f, 0.3f) << std::endl;  // true
    std::cout << are_equal(std::nanf(""), std::nanf("")) << std::endl;  // true
}

Когда прямое сравнение OK

// Если числа пришли из одного источника
float x = some_computation();
float y = x;  // y точно равен x
if (x == y) { }  // OK, не нужен epsilon

// Если используешь литералы (compile-time known)
float pi = 3.14159f;
if (x == pi) { }  // OK

// Если работаешь с целыми числами, представленными как float
float count = 5.0f;
if (count == 5.0f) { }  // OK

Best Practices

1. Используй комбинированный подход для production

template<typename T>
bool are_equal(T a, T b) {
    return std::abs(a - b) <= std::max(T(1e-8), 
           std::numeric_limits<T>::epsilon() * std::max(std::abs(a), std::abs(b)));
}

2. Документируй выбранный допуск

// Допуск 0.1% для финансовых расчётов
const float FINANCIAL_TOLERANCE = 0.001f;

3. Тестируй граничные случаи

// Очень маленькие числа
assert(float_equal(1e-10f, 2e-10f));

// Очень большие числа
assert(float_equal(1e6f, 1e6f + 1.0f));

// Близко к нулю
assert(float_equal(0.0f, 1e-9f));

4. Избегай сравнений в циклах

// ПЛОХО: Каждая итерация может иметь погрешность
float sum = 0.0f;
for (int i = 0; i < 1000; i++) {
    sum += 0.1f;
    if (sum == 100.0f) break;  // Никогда не сработает
}

// ХОРОШО: Проверка допуска
for (int i = 0; i < 1000; i++) {
    sum += 0.1f;
    if (float_equal(sum, 100.0f)) break;
}

Резюме

Не используй a == b для float!

Выбор метода:

  1. Epsilon абсолютный — для чисел близких к нулю
  2. Epsilon относительный — для больших чисел
  3. Комбинированный — для production (рекомендуется)
  4. ULP — когда нужна максимальная точность

Формула (рекомендуемая):

abs(a - b) < abs_tol OR abs(a - b) < rel_tol * max(abs(a), abs(b))

Выбор допусков зависит от контекста:

  • Научные вычисления: более строгие допуски (1e-10)
  • Graphics/gaming: более свободные (1e-4)
  • Финансы: специфичные (зависит от требований)