1

This is a follow up from an older question found here: Chaining Function Calls and user Mooing Duck provided me with an answer that works through the use of Proxy Class and Proxy functions. I have managed to template this class and it appears to be working. I'm getting completely different results between float and double...

Here are the non templated versions of the classes and application for floats and doubles:

Just replace all floats with doubles within the classes, functions, and proxy functions... The main program won't change except for the arguments.

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

Output for float:

-4.37114e-08,1

Output for double:

6.12323e-17,1

Here is the templated version...

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

The output for floats:

-4.37114e-08,1

The output for doubles:

6.12323e-17,1

This goes to show that the conversion of my class to a class template appears to be working. I understand that there may be a bit of precision lost due to conversion from double to float or widening from float to double when casting, however, I can't seem to wrap my mind around why there is such a difference in output values from one to the other...

The rotation of the point or vector {1,0} at 90 degrees or PI/2 radians should be {0,1}. I understand how floating-point arithmetic works and that the generated output for the x values is relatively close to 0 so they should be considered 0 for all tense and purposes and I can include the use an epsilon checking function to test if it is close enough to 0 to set it directly to 0 which is not an issue...

What intrigues my curiosity is why is it -4.3...e-8 for float and +6.1...e-17 for double? In the float case, I'm getting negative values, and for the double case, I'm getting positive values. In both cases yes they are extremely small and close to 0 which is fine, but opposite signs, that has me scratching my head?

I'm seeking clarity to get a better insight as to why these values are being generated the way they are... Is it coming from the type-conversion or is it due to the trig function that is being used? Or a combination of both? Just trying to pinpoint where the divergence of signs is coming from...

I need to be aware of what is causing this subtle difference as it will pertain to my usage of this class and its generated outputs when precision is preferred over good enough estimations.



Edit

When working with the instantiation of these function templates, specifically for the rotate function and I started to test <int> type for my vector objects... I started to get some compiler errors... The translate and scale functions were fine, I only had an issue with the rotate function due to similar reasons loss of data, narrowing and widening conversions, etc...

I had to change my rotate function's implemenation to this:

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

Here I'm forcing the angle to always be a double regardless of the type Ty. The rotate function still expects the same type for its argument as the type of the vec2_t object that is being instantiated. The issue was with the initialization of the vec2_t object that was being created and returned from the calculations. I had to explicitly static_cast the x and y coordinates to Ty. Now when I try the same program above for vec2_t<int> passing in a rotation value of 90 I am getting exactly 0,1 for my output.

Another interesting fact by forcing the angle to always be double and always casting the calculated values back to Ty, when I instantiate my vec2_t as either a double or float I'm always getting the positive 6.123...e-17 result back for both cases... This should also allow me to simplify the design of the is_zero() function to test if these values are close enough to 0 to set them explicitly to 0.

Francis Cugler
  • 7,788
  • 2
  • 28
  • 59
  • Since the template is working, you can remove the non-templated version of the code. Instead, it would be nice to add the few lines of code that instantiates with a `double` (instead of just writing a comment). – cigien Aug 01 '20 at 17:46
  • @cigien I was just showing the process of transforming my code from non-templated version to templated version during my refactoring stage... I showed both examples to show that the generated values are correct! – Francis Cugler Aug 01 '20 at 18:07
  • 1
    Ok, that's fine. Also note that you are doing a narrowing conversion. This won't compile in clang (and gcc warns). – cigien Aug 01 '20 at 18:18
  • 1
    This is exactly the kind of problem that "printf debugging" works well for. Print out the intermediate values, (like `a`) and the return values from `sin` and `cos`, and you have all the information you need to figure out what's wrong. – Marshall Clow Aug 01 '20 at 18:20

2 Answers2

1

TL;DR: Small numbers are close to zero whatever their sign. The numbers you got are "almost zero" given the circumstances.

I'd call this "sign obsession". Two very small numbers are similar even if their signs differ. Here you're looking at numbers at the edge of accuracy of the computations you performed. They are both equally "small", given their types. Other answer(s) give hints about where exactly is the clbuttic mistake :)

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
0

Your problem is in the line:

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

For the float case, this evaluates to 1.570796371

For the double case, this evaluates to 1.570796327

Marshall Clow
  • 15,972
  • 2
  • 29
  • 45
  • 1
    I understand that part, however, where is the difference in the `(-/+)` coming from? Is it the trig functions when applying those values? – Francis Cugler Aug 01 '20 at 18:09
  • Yes, because that value 1.5707.... is very close to PI/2 – Marshall Clow Aug 01 '20 at 18:15
  • 1
    You can see the difference is coming exactly from [cos](https://godbolt.org/z/vfdh9h). – cigien Aug 01 '20 at 18:17
  • 1
    (Somewhat luckily), with IEEE `binary64`, `90 * (PI / 180)` is exactly `PI/2`. Mathematically, `PI/2` is not `π/2`; it has a small error. `PI/2 ≈ π/2 - 6.1232e-17`. Since `PI / 2` is a little less than a quarter turn, `cos(PI/2)` is a tiny positive number. If you round `PI/2` to a `float` (as done by `*=`), then you round up to `float(PI/2) ≈ π/2 + 4.3711e-8`, which is a little more than a quarter turn, so `cos(float(PI/2))` is a tiny negative number. Fun fact: `cos(π/2 + e) = -sin(e) ≈ -e`, so the results in your question are the approximate errors `π/2 - PI/2` and `π/2 - float(PI/2)`. – HTNW Aug 01 '20 at 18:43
  • @HTNW I think what you just stated was exactly what I was looking for! As I stated in within the question, I wasn't worried about their actual values as they are both close enough to 0 that I can mask them to 0 if within some epsilon value range close to 0. I was just wanting to know for clarity sakes, how these variations were being generated - calculated... I wasn't sure if it was coming from the precision and bit representation of the data type or if it was coming from the actual trig function performing the calculations... – Francis Cugler Aug 01 '20 at 19:27
  • @HTNW (...continue) I knew that the trig functions were taking in slightly different numbers due to precision differences from `float` to `double`, the difference of input values is approximately `0.000000049` between the two values that are entered into the trig functions, and it was a curiosity to understand how that much of a difference can be the difference between a positive and a negative result. – Francis Cugler Aug 01 '20 at 19:27