Комментарии (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!
Выбор метода:
- Epsilon абсолютный — для чисел близких к нулю
- Epsilon относительный — для больших чисел
- Комбинированный — для production (рекомендуется)
- ULP — когда нужна максимальная точность
Формула (рекомендуемая):
abs(a - b) < abs_tol OR abs(a - b) < rel_tol * max(abs(a), abs(b))
Выбор допусков зависит от контекста:
- Научные вычисления: более строгие допуски (1e-10)
- Graphics/gaming: более свободные (1e-4)
- Финансы: специфичные (зависит от требований)