skip to Main Content

I have a templatized TRational class to handle fractional numbers for intergral types like signed/unsigned char/short/int/long long int etc. This works fine, but some struct functions require different code depending on the template type, like for signed or unsigned. Example:

template<typename T, typename = typename std::enable_if< std::is_integral<T>::value >::type> struct TRational
    {
    inline TRational() :
        m_Numerator(0), m_Denominator(0) { }
    inline TRational(T Numerator) :
        m_Numerator(Numerator), m_Denominator(1) { }
    inline TRational(T Numerator, T Denominator) :
        m_Numerator(Numerator), m_Denominator(Denominator) { }

    // Many functions...

    inline TRational<T> &ReduceMe()
        {
        bool Negative = false;
        if (std::is_signed<T>::value)
            {
            if (m_Numerator < 0)
                {
                Negative = !Negative;
                m_Numerator = -m_Numerator;
                }
            if (m_Denominator < 0)
                {
                Negative = !Negative;
                m_Denominator = -m_Denominator;
                }
            }
        T First = m_Numerator;
        T Second = m_Denominator;
        // Use Euclidean algorithm to find Greatest Common Divisor
        while (Second != 0)
            {
            T Temp = Second;
            Second = First % Second;
            First = Temp;
            }
        if (First > 1)
            {
            m_Numerator /= First;
            m_Denominator /= First;
            }
        if (std::is_signed<T>::value && Negative)
            m_Numerator = -m_Numerator;  // Reduced negative Rationals have negative Numerator
        return *this;
        }

    T m_Numerator;
    T m_Denominator;
    };

int main(int argc, char *argv[])
    {
    TRational<int> SignedRational(100,-10);
    TRational<unsigned short> UnsignedRational(5,100);
    
    SignedRational.ReduceMe();
    UnsignedRational.ReduceMe();

    printf("Signed Rational: (%d,%d)nUnsigned Rational: (%u,%u)n",
        SignedRational.m_Numerator,SignedRational.m_Denominator,
        UnsignedRational.m_Numerator,UnsignedRational.m_Denominator);
    }

This will compile on Xcode/Visual Studio/gcc (and optimize away as needed) and when run result in

Signed Rational: (-10,1)
Unsigned Rational: (1,20)

However, specially when compiling on Visual Studio (Windows) the compiler will complain:

warning C4146: unary minus operator applied to unsigned type, result still unsigned
message : while compiling class template member function 'TRational<unsigned int,void> &TRational<unsigned int,void>::ReduceMe(void)'

because the compiler will still compile the code within if (std::is_signed<T>::value) even in the unsigned ‘T’ case. Note the code within these blocks depends on the type of ‘T’.

How can I remove these warnings neatly in C++-11 (can’t use constexpr), preferably by specifying both a unsigned implementation and a (longer) signed implementation ?

I tried to search an answer (without constexpr from C++17) online, but couldn’t find a solution for this case. As the signed/unsigned code is not so large, I wouldn’t need a specialisation within the function. Ideally I would have hoped to find something like:

    // Something with std::is_unsigned<T>::value
    inline TRational<T> &ReduceMe()
        {
        // ... unsigned case code
        return *this;
        }
    // Something with std::is_signed<T>::value
    inline TRational<T> &ReduceMe()
        {
        // ... signed case (somewhat longer) code
        return *this;
        }

2

Answers


  1. In C++17 with if constexpr, if statement init, and std::gcd, you can reduce a lot:

    #include <numeric> // std::gcd
    
    
    template<typename T> constexpr T zeroAs = static_cast<T>(0);
    
    constexpr TRational<T>& ReduceMe()
    {
        if constexpr (std::is_signed_v<T>)
        {
            // first handle the m_Numerator
            if (m_Numerator = m_Numerator < zeroAs<T> ? m_Numerator : -m_Numerator;
                // now check for m_Denominator
                m_Numerator < zeroAs<T> && m_Denominator < zeroAs<T>)
            {
                m_Denominator = -m_Denominator;
            }
        }
    
        T gcd = std::gcd(m_Numerator, m_Denominator);
        m_Numerator /= gcd;
        m_Denominator /= gcd;
        return *this;
    }
    

    Live demo


    However, in C++11, you should be verbose everywhere, and not sure if it is shorter than your actual one:

    template<typename T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
    struct TRational
    {
        // other code ....
    
        TRational<T>& ReduceMe()
        {
            // Tag dispatch
            reduceImpl(std::integral_constant<bool, std::is_signed<T>::value>());
            
            T First = m_Numerator;
            T Second = m_Denominator;
    
            // Use Euclidean algorithm to find Greatest Common Divisor
            while (Second != static_cast<T>(0)) {
                T Temp = Second;
                Second = First % Second;
                First = Temp;
            }
            if (First > static_cast<T>(1)) {
                m_Numerator /= First;
                m_Denominator /= First;
            }
            return *this;
        }
    
    private:
        template<typename Type = T>
        void reduceImpl(std::true_type) {
            // Handle signed types
            m_Numerator = (m_Numerator < static_cast<T>(0)) ? m_Numerator : -m_Numerator;
            if (m_Numerator < static_cast<T>(0) && m_Denominator < static_cast<T>(0)) {
                m_Denominator = -m_Denominator;
            }
        }
    
        template<typename Type = T> void reduceImpl(std::false_type) {
            // Handle unsigned types
        }
    };
    

    Live demo

    Login or Signup to reply.
  2. I don’t see a lot of sense in allowing rationals over unsigned types. But if you really want to, you can. Just use SFINAE to provide separate implementations for signed and unsigned cases.

      template <typename K = T>
      inline typename std::enable_if<std::is_signed<K>::value, TRational<K>>::type &ReduceMe()
      {        
              // ... 
      }
     
      template <typename K = T>
      inline typename std::enable_if<std::is_unsigned<K>::value, TRational<K>>::type &ReduceMe()
      {
             // ...
      }
    

    (need an extra template parameter here because otherwise the compiler thinks its a pair of overloaded functions that only differ in their return type)

    Aside: do you really really want your default constructor to create 0/0?

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search