Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SFINAE differentiation between signed and unsigned

I have functions for converting different arithmetic types to a half precision floating point type (just a uint16_t on the lowest level) and I have different functions for integer and floating point source types, using SFINAE and std::enable_if:

template<typename T>
uint16_t to_half(typename std::enable_if<
                 std::is_floating_point<T>::value,T>::type value)
{
    //float to half conversion
}

template<typename T>
uint16_t to_half(typename std::enable_if<
                 std::is_integral<T>::value,T>::type value)
{
    //int to half conversion
}

These are called internally from a universal templated constructor by explicit instantiation:

template<typename T>
half::half(T rhs)
    : data_(detail::conversion::to_half<T>(rhs))
{
}

This compiles and also works just fine. Now I try to differentiate between signed and unsigned integers, by replacing the second function with the two functions:

template<typename T>
uint16_t to_half(typename std::enable_if<std::is_integral<T>::value &&
                 std::is_signed<T>::value,T>::type value)
{
    //signed to half conversion
}

template<typename T>
uint16_t to_half(typename std::enable_if<std::is_integral<T>::value &&
                 std::is_unsigned<T>::value,T>::type value)
{
    //unsigned to half conversion
}

But once I try to compile this VS2010 gives me

error C2995: "uint16_t math::detail::conversion::to_half( std::enable_if<std::tr1::is_integral<_Ty>::value && std::tr1::is_signed<_Ty>::value, T>::type )": function template already defined.

So it seems it cannot disambiguate between the two templates, but it obviously had no problems with the integral version alongside the floating point version.

But since I'm not that much a a template magician I may just be missing something obvious here (or maybe it should actually work and is just a VS2010 bug). So why doesn't this work and how can it be made work with as few programming overhead as possible and in the limits of standard-only features (if even possible)?

like image 528
Christian Rau Avatar asked Feb 14 '12 23:02

Christian Rau


3 Answers

Personally, I would avoid SFINAE here as much as possible since you can accomplish the same thing with overloading:

template<typename T>
uint16_t to_half_impl(T val, std::true_type, std::true_type)
{
    // is_integral + is_signed implementation
}

template<typename T>
uint16_t to_half_impl(T val, std::true_type, std::false_type)
{
    // is_integral + is_unsigned implementation
}

template<typename T>
uint16_t to_half_impl(T val, std::false_type, std::true_type)
{
    // is_floating_point implementation
}

template<typename T>
typename std::enable_if<std::is_arithmetic<T>::value, uint16_t>::type to_half(T val)
{
    return to_half_impl(val, std::is_integral<T>(), std::is_signed<T>());
}
like image 198
ildjarn Avatar answered Oct 16 '22 21:10

ildjarn


If this doesn't work then your compiler is at error.

Two expressions involving template parameters are considered equivalent if two function definitions containing the expressions would satisfy the one definition rule ...

That's the most important rule to consider here (left out the details of "..."). Your two templates do not satisfy the ODR because their token sequences differ.

Two function templates are equivalent if they are declared in the same scope, have the same name, have identical template parameter lists, and have return types and parameter lists that are equivalent using the rules described above to compare expressions involving template parameters.

So your two templates define different templates and do not clash. You could now check whether your templates perhaps are "functionally equivalent". They would be if for any possible set of template arguments, your enable_if expression would always yield the same value. But since that is not true for is_unsigned and is_signed, this is not the case either. If it would, then your code would be ill-formed, but without requiring a diagnostic (which effectively means "undefined behavior").

like image 23
Johannes Schaub - litb Avatar answered Oct 16 '22 20:10

Johannes Schaub - litb


The more common idiom is to use SFINAE on the return type rather than the argument type. Otherwise, the template type T may not be deducible. With

// from C++14
template<bool C, typename T> using enable_if_t = typename std::enable_if<C,T>::type;

template<typename T>
enable_if_t<std::is_integral<T>::value &&  std::is_signed<T>::value, uint16_t>
to_half(T value)
{
    //signed to half conversion
}

template<typename T>
enable_if_t<std::is_integral<T>::value && !std::is_signed<T>::value,  int16_t>
to_half(T value)
{
    //unsigned to half conversion
}

the type T in the following statement

auto y=to_half(x);    // T is deduced from argument, no need for <T> 

is deducible (even trivially), but for your original code it is not! Indeed, when running this statement with your to_half() implementation through clang gives

test.cc:24:11: error: no matching function for call to 'to_half'
  auto x= to_half(4);
          ^~~~~~~
test.cc:7:10: note: candidate template ignored: couldn't infer template argument 'T'
uint16_t to_half(typename std::enable_if<std::is_integral<T>::value &&
         ^
test.cc:15:10: note: candidate template ignored: couldn't infer template argument 'T'
uint16_t to_half(typename std::enable_if<std::is_integral<T>::value &&
         ^

Of course, if one explicitly provides the template argument (as you did), this problem does not appear. So your code wasn't wrong (but the compiler), but what's the point of SFINAE if you pass the template argument type?

like image 1
Walter Avatar answered Oct 16 '22 21:10

Walter