Search code examples
c++c++11templatesspecializationclass-template

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


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

Solution

  • 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?