8

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

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Christian Rau
  • 45,360
  • 10
  • 108
  • 185
  • 1
    It's not clear that `is_signed`/`is_unsigned` is mutually exclusive (hello `char`?). Try to make the second version say `!std::is_signed::value` instead. – Kerrek SB Feb 14 '12 at 23:21
  • Can you try to use `std::is_signed::value` for one of the members and `!std::is_signed::value` for the other? This is just to make sure that there isn't just some type which has inconsistent settings for `is_signed` and `is_unsigned`. – Dietmar Kühl Feb 14 '12 at 23:22
  • @KerrekSB & Dietmar Hah, that did it! Can't believe it was that easy. If someone adds it as an answer I'll accept it. – Christian Rau Feb 14 '12 at 23:26
  • @Kerrek `char` is neither a signed integer type nor an unsigned integer type. But IIRC `is_signed` and `is_unsigned` take care of that: Only one of them will report `true` for `char`. – Johannes Schaub - litb Feb 14 '12 at 23:29
  • @JohannesSchaub-litb: You're right, `char` is OK. Enums and pointers are always false, though; I guess because they're not arithmetic types. – Kerrek SB Feb 14 '12 at 23:33

3 Answers3

8

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>());
}
ildjarn
  • 62,044
  • 9
  • 127
  • 211
  • +1 Nice alternative. But as a side note I think the last argument to the float version should be of `std::true_type`, as floats are always signed (at least the usual implementations, for non-IEEE my conversion code won't work anyway). – Christian Rau Feb 15 '12 at 00:17
  • @Christian : You're totally correct; I was basing the logic off of the MSDN docs for `is_signed`, which happen to be wrong (surprise, surprise). Fixed. – ildjarn Feb 15 '12 at 00:26
3

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").

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • Replacing `is_unsigned` with `!is_signed` (or vice versa) worked, so I guess (although being not that well versed in the depths of the language spec, not to speak of templates) that there is some type that is both signed and usigned. Or could it be because the default versions of those templates both evaluate to `false` for non-arithmetic types? But then again, there is also the `is_integral` to disambiguate things (for integral types it shoudl be mutually exclusive, shouldn't it?). – Christian Rau Feb 14 '12 at 23:37
  • @Christian I have no idea what they are doing, but that's definitely wrong behavior. Try `!!is_unsigned` instead of `!is_signed`. I wouldn't be surprised to see it "working" too :) – Johannes Schaub - litb Feb 14 '12 at 23:38
  • Hah, that worked, too. Ok now I think it really gets a bit ridiculous. You're probably right in that the compiler is at fault here. – Christian Rau Feb 14 '12 at 23:41
  • "Two expressions involving template parameters are considered equivalent if two **function definitions containing the expressions** would satisfy the one definition rule ..." Could you explain this further by adding an example in your post? – Prasoon Saurav Feb 22 '12 at 08:02
  • Your answer is only half correct. The compiler was correct to reject the code (see my answer below), but it gave the wrong reason. – Walter Apr 26 '14 at 10:23
  • @walter he explicitly passes the argument – Johannes Schaub - litb Apr 26 '14 at 10:34
  • Indeed he does. Strange. What's the point of SFINAE if you do that? – Walter Apr 26 '14 at 11:16
1

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?

Walter
  • 44,150
  • 20
  • 113
  • 196
  • Shouldn't this have the same ambiguity problems as the SFINAE in the argument type? Is there some rule that makes this work but the argument version not? Otherwise it doesn't seem to adress the question so much. – Christian Rau Apr 26 '14 at 01:11
  • There is a **very good reason** not to use SFINAE on the argument type, see edited answer. – Walter Apr 26 '14 at 10:24