【问题标题】:Subtle differences in output values (+/-) between float and doublesfloat 和 double 之间的输出值 (+/-) 的细微差别
【发布时间】:2020-11-22 06:11:54
【问题描述】:

这是对此处找到的一个较旧问题的跟进:Chaining Function Calls 和用户Mooing Duck 为我提供了一个通过使用代理类和代理函数的答案。我已经设法为这个类模板,它似乎正在工作。我在floatdouble 之间得到完全不同的结果...

以下是floatsdoubles应用程序 的非模板版本:

只需将函数代理函数中的所有floats替换为doubles...主要除了参数,程序不会改变。

#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;

浮点输出:

-4.37114e-08,1

双精度输出:

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}。我了解浮点运算的工作原理,并且为x 值生成的输出相对接近0,因此对于所有时态和目的,它们应该被视为0,我可以包括使用epsilon 检查函数来测试它是否足够接近 0 以将其直接设置为 0 这不是问题...

让我好奇的是,为什么 -4.3...e-8 代表 float 而 +6.1...e-17 代表 double?在 float 情况下,我得到负值,而对于 double 情况,我得到正值。在这两种情况下,是的,它们都非常小并且接近 0,这很好,但相反的迹象,这让我摸不着头脑?

我正在寻求澄清,以便更好地了解为什么这些值会以它们的方式生成......它来自类型转换还是由于正在使用的 trig 函数?还是两者兼而有之?只是试图查明迹象的分歧来自哪里......

我需要了解导致这种细微差异的原因,因为当精度优于足够好的估计时,这与我对此类的使用及其生成的输出有关。



编辑

在处理这些函数模板的实例化时,特别是对于旋转函数,我开始为我的向量对象测试&lt;int&gt; 类型...我开始遇到一些编译器错误...翻译和缩放函数是好吧,由于类似的原因loss of datanarrowingwidening 转换等,我只遇到了旋转功能的问题...

我不得不将我的旋转功能的实现更改为:

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) ) 
                     };
}

在这里,无论Ty 的类型如何,我都强制角度始终为double。 rotate 函数仍期望其参数的类型与正在实例化的 vec2_t 对象的类型相同。问题在于正在创建并从计算中返回的 vec2_t 对象的初始化。我必须明确地将static_cast xy 坐标到Ty。现在,当我为vec2_t&lt;int&gt; 尝试上面的相同程序时,传入90 的旋转值,我得到的输出正好是0,1

当我将vec2_t 实例化为doublefloat 时,另一个有趣的事实是强制角度始终为double 并始终将计算值转换回Ty,我总是得到两种情况下的正面6.123...e-17 结果...这也应该允许我简化is_zero() 函数的设计,以测试这些值是否足够接近0 以将它们显式设置为0

【问题讨论】:

  • 由于模板正在工作,您可以删除代码的非模板版本。相反,最好添加几行用double 实例化的代码(而不仅仅是写评论)。
  • @cigien 我只是展示了在重构阶段将我的代码从非模板版本转换为模板版本的过程......我展示了两个示例以表明生成的值是正确的!跨度>
  • 好的,没关系。另请注意,您正在进行缩小转换。这不会在 clang 中编译(并且 gcc 会发出警告)。
  • 这正是“printf 调试”适用的问题。打印出中间值(如a)和sincos 的返回值,您就拥有了找出问题所在的所有信息。

标签: c++ casting output c++17 precision


【解决方案1】:

TL;DR:无论符号如何,小数都接近于零。在这种情况下,你得到的数字“几乎为零”。

我称之为“标志痴迷”。两个非常小的数是相似的,即使它们的符号不同。在这里,您正在查看您执行的计算的准确性边缘的数字。考虑到它们的类型,它们都同样“小”。其他答案提示了错误的确切位置:)

【讨论】:

    【解决方案2】:

    你的问题是:

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

    对于 float 的情况,计算结果为 1.570796371

    对于 double 情况,计算结果为 1.570796327

    【讨论】:

    • 我理解那部分,但是,(-/+) 的区别来自哪里?应用这些值时是三角函数吗?
    • 是的,因为该值 1.5707.... 非常接近 PI/2
    • 您可以看到差异完全来自cos
    • (有点幸运),对于 IEEE binary6490 * (PI / 180) 正好是 PI/2。在数学上,PI/2 不是π/2;它有一个小错误。 PI/2 ≈ π/2 - 6.1232e-17。由于PI / 2 不到四分之一圈,cos(PI/2) 是一个很小的正数。如果你将PI/2 舍入为float(由*= 完成),那么你将舍入为float(PI/2) ≈ π/2 + 4.3711e-8,这比四分之一转多一点,所以cos(float(PI/2)) 是一个很小的负数。有趣的事实:cos(π/2 + e) = -sin(e) ≈ -e,所以您问题中的结果是近似错误π/2 - PI/2π/2 - float(PI/2)
    • @HTNW 我认为你刚才所说的正是我想要的!正如我在问题中所说,我并不担心它们的实际值,因为它们都足够接近 0,如果在接近 0 的某个 epsilon 值范围内,我可以将它们屏蔽为 0。我只是想知道清楚天啊,这些变化是如何产生的——计算出来的......我不确定它是来自数据类型的精度和位表示,还是来自执行计算的实际触发函数......跨度>
    猜你喜欢
    • 1970-01-01
    • 2017-04-28
    • 2011-04-26
    • 1970-01-01
    • 2010-11-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-22
    相关资源
    最近更新 更多