Template structure function specialisation depending on template type in C++11

99 Views Asked by At

I have a templatized TRational class to handle fractional numbers for integral 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 (can't use constexpr), preferably by specifying both an unsigned implementation and a (longer) signed implementation?

I tried to search an answer (without if 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

There are 2 best solutions below

2
n. m. could be an AI On BEST ANSWER

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?

5
JeJo On

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