Всегда ли приведение 'std:: floor ()' и 'std:: ceil ()' к целочисленному типу дает правильный результат?


Я параноик, что одна из этих функций может дать неправильный результат, как это:

std::floor(2000.0 / 1000.0) --> std::floor(1.999999999999) --> 1
or
std::ceil(18 / 3) --> std::ceil(6.000000000001) --> 7

Может ли произойти нечто подобное? Если действительно существует такой риск, я планирую использовать функции ниже, чтобы работать безопасно. Но так ли уж это необходимо?

constexpr long double EPSILON = 1e-10;

intmax_t GuaranteedFloor(const long double & Number)
{
    if (Number > 0)
    {
        return static_cast<intmax_t>(std::floor(Number) + EPSILON);
    }
    else
    {
        return static_cast<intmax_t>(std::floor(Number) - EPSILON);
    }
}

intmax_t GuaranteedCeil(const long double & Number)
{
    if (Number > 0)
    {
        return static_cast<intmax_t>(std::ceil(Number) + EPSILON);
    }
    else
    {
        return static_cast<intmax_t>(std::ceil(Number) - EPSILON);
    }
}

(примечание: Я предполагаю, что данный аргумент 'long double' будет соответствовать возвращаемому типу 'intmax_t'.)

4 12

4 ответа:

У людей часто складывается впечатление, что операции с плавающей запятой дают результаты с небольшими, непредсказуемыми, квази-случайными ошибками. Это впечатление неверно.

Вычисления с плавающей точкой являютсянастолько точными, насколько это возможно . 18/3 всегда будет производить ровно 6 . Результат 1/3 не будет точно одной третью, но это будет самое близкое число к одной трети, которое можно представить как число с плавающей точкой.

Таким образом, примеры, которые вы показали, гарантированно всегда работать. Что касается предложенного вами "гарантированного пола/ceil", это не очень хорошая идея. Некоторые последовательности операций могут легко взорвать ошибку намного выше 1e-10, а некоторые другие случаи использования потребуют, чтобы 1e-10 был правильно распознан (и ceiled) как ненулевой.

Как правило, жестко закодированные значения Эпсилона являются ошибками в вашем коде.

В конкретных примерах, которые вы перечисляете, я не думаю, что эти ошибки когда-либо возникнут.

std::floor(2000.0 /*Exactly Representable in 32-bit or 64-bit Floating Point Numbers*/ / 1000.0 /*Also exactly representable*/) --> std::floor(2.0 /*Exactly Representable*/) --> 2
std::ceil(18 / 3 /*both treated as ints, might not even compile if ceil isn't properly overloaded....?*/) --> 6
std::ceil(18.0 /*Exactly Representable*/ / 3.0 /*Exactly Representable*/) --> 6

Сказав, что, если у вас есть математика, которая зависит от этих функций, ведущих себя точно правильно для чисел с плавающей запятой, это может осветить недостаток дизайна, который вам нужно пересмотреть/пересмотреть.

Пока значения с плавающей запятой x и y точно представляют целые числа в пределах типа, который вы используете, нет никаких проблем - x / y всегда будет давать значение с плавающей запятой, которое точно представляет целочисленный результат. приведение к int, как вы делаете, всегда будет работать.

Однако, как только значения с плавающей запятой выходят за пределы целочисленного представимого диапазона для типа (, представляющего целые числа в двойниках ), эпсилоны не помогают.

Рассмотрим это образец. 16777217-наименьшее целое число, которое не может быть точно представлено в виде 32-разрядного float:

int ix=16777217, iy=97;
printf("%d / %d = %d", ix, iy, ix/iy);
// yields "16777217 / 97 = 172961" which is accurate

float x=ix, y=iy;
printf("%f / %f = %f", x, y, x/y);
// yields "16777216.000000 / 97.000000 = 172960.989691"
В этом случае ошибка отрицательна; в других случаях (попробуйте 16777219 / 1549) ошибка положительна.

Хотя и заманчиво добавить Эпсилон, чтобы заставить floor работать, это не сильно увеличит точность. Когда значения отличаются более чем на порядки, ошибка становится больше 1 и целочисленная точность не может быть гарантирована. в частности, когда x/y превышает макс. представимо, ошибка может превышать 1,0, так что Эпсилон не поможет.

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

Такие результаты, скорее всего, появятся при работе с двойниками. Вы можете использовать раунд или вычесть 0.5, а затем использовать функцию std::ceil.