Skip to content

Latest commit

 

History

History
99 lines (72 loc) · 5.67 KB

floats.md

File metadata and controls

99 lines (72 loc) · 5.67 KB

Числа с плавающей точкой

С float и double в принципе всегда все сложно. Особенно в C++.

Стандарт C++ не требует следования стандарту IEEE 754, потому деление на ноль в вещественных числах также считается неопределенным поведением, несмотря на то, что по IEEE 754 выражение x/0.0 определяется как -INF, NaN, или INF в зависимости от знака числа x (NaN для нуля).

Сравнение вещественных чисел — излюбленная головная боль.

Выражение x == y фактически является кривым побитовым сравнением для чисел с плавающей точкой, по особенному работающее со случаями -0.0 и +0.0, и NaN. О существовании этого и != операторов для вещественных чисел стоит забыть и никогда не вспоминать.

Для побитового сравнения нужно использовать memcmp. Для сравнения чисел — приближенные варианты вида std::abs(x - y) < EPS, где EPS — какое-то абсолютное или вычисляемое на основе x и y значение. А также различные манипуляции с ULP сравниваемых чисел.

Так как стандарт C++ не форсирует IEEE 754, проверки на x == NaN через его свойство (x != x) == true могут быть убраны компилятором, как заведомо ложные. Проверять нужно с помощью предназначенных для этого функций std::isnan.

Поддерживается или нет IEEE 754 можно проверить с помощью предопределенной константы std::numeric_limits<FloatType>::is_iec559

Сужающие преобразования из float в знаковые или беззнаковые целые могут повлечь неопределенное поведение, если значение непредставимо в целочисленном типе. Никаких обрезок по модулю 2^N не предполагается.

constexpr uint16_t x = 1234567.0; // CE, undefined behavior

Обратное преобразование, из целочисленных типов во float/double, также имеет свои подвохи, не связанные с неопределенным поведением: большие по абсолютной величине целые числа теряют точность

static_assert(
    static_cast<float>(std::numeric_limits<int>::max()) == 
    static_cast<float>(static_cast<long long>(std::numeric_limits<int>::max()) + 1) // OK
);

static_assert(
    static_cast<double>((1LL << 53) - 1) == 
    static_cast<double>(1LL << 53)  // fire!
);

static_assert(
    static_cast<double>((1LL << 54) - 1) == 
    static_cast<double>(1LL << 54) // OK
);

static_assert(
    static_cast<double>((1LL << 55) - 1) == 
    static_cast<double>(1LL << 55) // OK
);

static_assert(
    static_cast<double>((1LL << 56) - 1) == 
    static_cast<double>(1LL << 56) // OK
);

В качестве домашнего задания читателю предлагается самостоятельно сформулировать, почему никогда нельзя хранить деньги в типах с плавающей запятой.

Плавающая точка и шаблоны

Вещественные числа до C++20 нельзя было использовать в качестве параметров-значений в шаблонах. Теперь же можно. Правда, ожидать, что вы насчитаете в run-time и в compile-time одно и то же — не стоит.

Для простой параметризации типов константами этот механизм вполне можно использовать без опасений. Однако строить на них паттерн-матчинг с выбором специализаций шаблонов крайне не рекомендуется:

template <double x>
struct X {
    static constexpr double val = x;
};

template <>
struct X<+0.> {
    static constexpr double val = 1.0;
};

template <>
struct X<-0.> {
    static constexpr double val = -1.0;
};


int main() {
    constexpr double a = -3.0;
    constexpr double b = 3.0;
    std::cout << X<a + b>::val << "\n";          // печатает +1
    std::cout << X<-1.0 * (a + b)>::val << "\n"; // печатает -1
    static_assert(a + b == -1.0 * (a + b));      // ok
}

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

Полезные ссылки

  1. https://en.cppreference.com/w/cpp/numeric/math/isnan
  2. https://bitbashing.io/comparing-floats.html
  3. https://diego.assencio.com/?index=67e5393c40a627818513f9bcacd6a70d