Незначительные различия в выходных значениях (+/-) между float и double - PullRequest
1 голос
/ 01 августа 2020

Это продолжение более старого вопроса, найденного здесь: Цепочка вызовов функций и пользователь Mooing Duck предоставил мне ответ, который работает с использованием функций Proxy Class и Proxy. . Мне удалось создать шаблон этого класса, и он, похоже, работает. Я получаю совершенно разные результаты между float и double ...

Вот не шаблонные версии классов и приложения для floats и doubles:

Просто замените все float s на double s внутри классов , функций и прокси-функций ... Основная программа не изменится, за исключением аргументов .

#include <cmath>
#include <exception>
#include <iostream>
#include <utility>

namespace pipes {
          
    const double PI = 4 * atan(1);

    struct vec2 {
        float x;
        float y;
    };

    std::ostream& operator<<(std::ostream& out, vec2 v2) {
        return out << v2.x << ',' << v2.y;
    }
 
    vec2 translate(vec2 in, float a) {
        return vec2{ in.x + a, in.y + a };
    }

    vec2 rotate(vec2 in, float a) {
        // convert a in degrees to radians:
        a *= (float)(PI / 180.0);
        return vec2{ in.x*cos(a) - in.y*sin(a),
            in.x*sin(a) + in.y*cos(a) };
    }

    vec2 scale(vec2 in, float a) {
        return vec2{ in.x*a, in.y*a };
    }    

    // proxy class
    template<class rhst, vec2(*f)(vec2, rhst)>
    class vec2_op1 {
        std::decay_t<rhst> rhs; // store the parameter until the call
    public:
        vec2_op1(rhst rhs_) : rhs(std::forward<rhst>(rhs_)) {}
        vec2 operator()(vec2 lhs) { return f(lhs, std::forward<rhst>(rhs)); }
    };

    // proxy methods      
    vec2_op1<float, translate> translate(float a) { return { a }; }
    vec2_op1<float, rotate> rotate(float a) { return { a }; }
    vec2_op1<float, scale> scale(float a) { return { a }; }

    // lhs is the object, rhs is the operation on the object
    template<class rhst, vec2(*f)(vec2, rhst)>
    vec2& operator|(vec2& lhs, vec2_op1<rhst, f>&& op) { return lhs = op(lhs); }

} // namespace pipes

int main() {
    try {
        pipes::vec2 a{ 1.0, 0.0 };
        pipes::vec2 b = (a | pipes::rotate(90.0));
        std::cout << b << '\n';
    } catch (const std::exception& e) {
    std::cerr << e.what() << "\n\n";
    return EXIT_FAILURE;
}
return EXIT_SUCCESS;

Вывод для float:

-4.37114e-08,1

Вывод для double:

6.12323e-17,1

Вот шаблонная версия ...

#include <cmath>
#include <exception>
#include <iostream>
#include <utility>

namespace pipes {
          
    const double PI = 4 * atan(1);

    template<typename Ty>
    struct vec2_t {
        Ty x;
        Ty y;
    };

    template<typename Ty>
    std::ostream& operator<<(std::ostream& out, vec2_t<Ty> v2) {
        return out << v2.x << ',' << v2.y;
    }

    template<typename Ty>
    vec2_t<Ty> translate(vec2_t<Ty> in, Ty a) {
        return vec2_t<Ty>{ in.x + a, in.y + a };
    }

    template<typename Ty>
    vec2_t<Ty> rotate(vec2_t<Ty> in, Ty a) {
        // convert a in degrees to radians:
        a *= (Ty)(PI / 180.0);
        return vec2_t<Ty>{ in.x*cos(a) - in.y*sin(a),
                     in.x*sin(a) + in.y*cos(a) };
    }

    template<typename Ty>
    vec2_t<Ty> scale(vec2_t<Ty> in, Ty a) {
        return vec2_t<Ty>{ in.x*a, in.y*a };
    }

    // proxy class
    template<class rhst, typename Ty, vec2_t<Ty>(*f)(vec2_t<Ty>, rhst)>
    class vec2_op1 {
        std::decay_t<rhst> rhs; // store the parameter until the call
    public:
        vec2_op1(rhst rhs_) : rhs(std::forward<rhst>(rhs_)) {}
        vec2_t<Ty> operator()(vec2_t<Ty> lhs) { return f(lhs, std::forward<rhst>(rhs)); }
    };

    // proxy methods
    template<typename Ty>
    vec2_op1<Ty, Ty, translate<Ty>> translate(Ty a) { return { a }; }
    template<typename Ty>
    vec2_op1<Ty, Ty, rotate<Ty>> rotate(Ty a) { return { a }; }
    template<typename Ty>
    vec2_op1<Ty, Ty, scale<Ty>> scale(Ty a) { return { a }; }

    // overloaded | operator for chaining function calls to vec2_t objects
    // lhs is the object, rhs is the operation on the object
    template<class rhst, typename Ty, vec2_t<Ty>(*f)(vec2_t<Ty>, rhst)>
    vec2_t<Ty>& operator|(vec2_t<Ty>& lhs, vec2_op1<rhst, Ty, f>&& op) { return lhs = op(lhs); }

} // namespace pipes

// for double just instantiate with double... 
int main() {
    try {    
        pipes::vec2_t<float> a{ 1.0f, 0.0f };
        pipes::vec2_t<float> b = (a | pipes::rotate(90.0f));    
        std::cout << b << '\n';    
    } catch (const std::exception& e) {
        std::cerr << e.what() << "\n\n";
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Вывод для чисел с плавающей запятой:

-4.37114e-08,1

Вывод для удвоений:

6.12323e-17,1

Это говорит о том, что преобразование моего класса в шаблон класса, похоже, работает. Я понимаю, что может быть небольшая потеря точности из-за преобразования из double в float или расширения с float до double при кастинге, однако я не могу понять, почему существует такая разница в выходных значениях от одного к другому ...

Поворот точки или вектора {1,0} на 90 градусов или PI / 2 радиан должен быть {0,1}. Я понимаю, как работает арифметика с плавающей запятой c и что сгенерированный вывод для значений x относительно близок к 0, поэтому их следует рассматривать 0 для любого времени и целей, и я могу включить использование epsilon функция проверки, чтобы проверить, достаточно ли оно близко к 0, чтобы установить его прямо на 0, что не является проблемой ...

Меня заинтриговало мое любопытство, почему это -4.3...e-8 для float и +6.1...e-17 для двойного? В случае с плавающей точкой я получаю отрицательные значения, а в случае двойного значения - положительные. В обоих случаях да, они очень маленькие и близки к 0, что нормально, но противоположные признаки, из-за которых я чешу затылок?

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

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



Редактировать

При работе с экземплярами этих шаблонов функций, особенно для функции поворота, я начал test <int> type для моих векторных объектов ... Я начал получать некоторые ошибки компилятора ... Функции перевода и масштабирования были в порядке, у меня была проблема только с функцией поворота по тем же причинам loss of data, narrowing и widening преобразований и т.д. c ...

Мне пришлось изменить реализацию моей функции поворота на это:

template<typename Ty>
vec2_t<Ty> rotate(vec2_t<Ty> in, Ty a) {
    // convert a in degrees to radians:
    auto angle = (double)(a * (PI / 180.0));
    return vec2_t<Ty>{ static_cast<Ty>( in.x*cos(angle) - in.y*sin(angle) ),
                       static_cast<Ty>( in.x*sin(angle) + in.y*cos(angle) ) 
                     };
}

Здесь я заставляю угол всегда быть double независимо от типа Ty. Функция поворота по-прежнему ожидает в качестве аргумента того же типа, что и тип объекта vec2_t, экземпляр которого создается. Проблема заключалась в инициализации объекта vec2_t, который создавался и возвращался в результате вычислений. Мне пришлось явно указать static_cast координаты x и y на Ty. Теперь, когда я пробую ту же программу для vec2_t<int>, передавая значение поворота 90, я получаю ровно 0,1 для своего вывода.

Еще один интересный факт, заставляя угол всегда быть double и всегда возвращая вычисленные значения обратно к Ty, когда я создаю свой vec2_t как double или float, я всегда получаю положительный результат 6.123...e-17 для обоих случаев ... Это также должно позволить мне упростить дизайн функции is_zero(), чтобы проверить, достаточно ли близки эти значения к 0, чтобы явно установить их на 0.

Ответы [ 2 ]

1 голос
/ 01 августа 2020

TL; DR: Маленькие числа близки к нулю независимо от их знака. Полученные вами числа «почти нулевые», учитывая обстоятельства.

Я бы назвал это «одержимостью знаками». Два очень маленьких числа похожи, даже если их знаки различаются. Здесь вы смотрите на числа на грани точности выполненных вами вычислений. Они оба одинаково «маленькие», учитывая их типы. Другой ответ (ы) дает подсказку о том, где именно находится ошибка clbutti c :)

0 голосов
/ 01 августа 2020

Ваша проблема заключается в строке:

    a *= (Ty)(PI / 180.0);

Для случая float это оценивается как 1,570796371

Для случая double это оценивается как 1,570796327

...