3

This is a question about how template deduction works when template parameter used as template parameter vs as default template parameter vs as return type.

1: plain template parameter

As tested using GCC and VS compiliation of below snippet fails to deduce template parameter defined by std::enable_if_t

#include <iostream>
#include <type_traits>

template< class T, std::enable_if_t< std::is_integral_v< T > > >
void SwapInPlace( T& left, T& right )
{
   left = left ^ right;
   right = left ^ right;
   left = left ^ right;
} 

template< class T, std::enable_if_t< std::is_floating_point_v< T > > >
void SwapInPlace( T& left, T& right )
{
    left = left - right;
    right = left + right;
    left = right - left;
}

int main()
{   
   int i1 = 10;
   int i2 = -120;
   std::cout << i1 << " " << i2 << std::endl;
   SwapInPlace( i1, i2 );
   std::cout << i1 << " " << i2 << std::endl;

   double d1 = 1.1234;
   double d2 = 2.5678;  
   std::cout << d1 << " " << d2 << std::endl;
   SwapInPlace( d1 , d2 );
   std::cout << d1 << " " << d2 << std::endl;

   return 0;
}

VS: error C2783: 'void SwapInPlace(T &,T &)': could not deduce template argument for '__formal'

GCC: couldn't deduce template parameter -anonymous-

2: default template parameter

Declaring second template parameter as default template paramater makes deduction to work ok:

template< class T, class = std::enable_if_t< std::is_integral_v< T > > >
void SwapInPlace( T& left, T& right )
{...}

template< class T, class = std::enable_if_t< std::is_floating_point_v< T > >, bool = true >
void SwapInPlace( T& left, T& right )
{...}

Added bool = true to second overload just to avoid compilation error because it's not possible to overload function template based on default template parameter. Want to focus only on the fact that deduction works ok here. If there was only one template using default parameter, e.g. for std::is_integral, it would compile and work fine upon condition we pass correct parameters to it.

3: return type

In case of return type everything compiles and works well:

template< class T >
std::enable_if_t < std::is_integral_v< T > >
SwapInPlace( T& left, T& right )
{...}

template< class T >
std::enable_if_t < std::is_floating_point_v< T > >
SwapInPlace( T& left, T& right )
{...}

4: template parameter with default value

Another way to make this code compile is by adding default value for template parameter defined by std::enable_if. Added * = nullptr before last closing angle bracket, so if std::enable_if condition evaluates to true then our second parameter becomes void* with default value nullptr:

template< class T, std::enable_if_t< std::is_integral_v< T > >* = nullptr >
void SwapInPlace( T& left, T& right )
{...}

template< class T, std::enable_if_t< std::is_floating_point_v< T > >* = nullptr >
void SwapInPlace( T& left, T& right )
{...}

So the question is: how deduction works in this 4 cases: why it fails in first case and succeds in three other?

max66
  • 65,235
  • 10
  • 71
  • 111
Soup Endless
  • 439
  • 3
  • 10
  • 1
    It doesn't have anything to do with `enable_if` so you can simplify the question considerably. You wonder why `template` won't work, but why `template` does, right? – Ted Lyngmo Feb 20 '21 at 10:48
  • @TedLyngmo Not exactly, I wonder why it's working and why it's not working for the cases I listed. I most curios in why this is working for the case when default template parameter is used. But I still want to understand all other cases too. – Soup Endless Feb 20 '21 at 10:51
  • Yes, and I say that you get exactly the same _without_ mixing in `enable_if`. [example](https://godbolt.org/z/z8h71o) – Ted Lyngmo Feb 20 '21 at 10:52
  • @TedLyngmo sure. I need to rewrite the question then. – Soup Endless Feb 20 '21 at 10:53

2 Answers2

4

how deduction works in this 4 cases: why it fails in first case and succeds in three other?


First case

template< class T, std::enable_if_t< std::is_integral_v< T > > >
void SwapInPlace( T& left, T& right )

Suppose std::is_integral_v is true; with the std::enable_if_t substitution you get

template< class T, void>
void SwapInPlace( T& left, T& right )

that isn't a valid C++ code, because it's requested a void value (for a second template parameter) but void can't have a valid value.

Suppose you substitute a int (a type that accept a valid value) for the type of the second template parameter

// .........................................................VVVVVV
template< class T, std::enable_if_t< std::is_integral_v< T >, int > >
void SwapInPlace( T& left, T& right )

in case of integral T value you get

template <class T, int>
void SwapInPlace( T& left, T& right )

that is valid but... suppose the call is

int a{1}, b{2};

SwapInPlace(a, b); // compilation error

You have that T is deduced as int from a and b but the compiler can't decide a value for the second (un-named) template parameter.

So, to make a valid call, you have to explicit the second template parameter calling the function

SwapInPlace<int, 0>(a, b); // OK

This way the call works because no deduction take place and explicitly say that T is int and the int parameter is 0.

This works but is uncomfortable, because you have to explicit the T that can be deduced.

To avoid this problem, you can add a default value for the second template parameter

// .........................................................VVVVVV.VVVV
template< class T, std::enable_if_t< std::is_integral_v< T >, int > = 0 >
void SwapInPlace( T& left, T& right )

so, in case of T integral, you get

// ....................VVVV
template <class T, int = 0>
void SwapInPlace( T& left, T& right )

and now the simple call

int a{1}, b{2};

SwapInPlace(a, b); // OK now

works because T is deduced as int and the second template parameter is defaulted to zero.

Works but you can observe that you're in the fourth case, now (with int instead of void * and 0 instead of nullptr)


Second case

The second case is a bad idea.

template< class T, class = std::enable_if_t< std::is_integral_v< T > > >
void SwapInPlace( T& left, T& right )
{...}

When T is integral become

template< class T, class = void>
void SwapInPlace( T& left, T& right )
{...}

that is a valid code, with T that is deducible from left and right arguments and the second, un-named, argument is defaulted, so there is no needs to explicit it.

But when T isn't integral, the SFINAE failure throw away only the default value for the second template parameter, so you get

template< class T, class>
void SwapInPlace( T& left, T& right )
{...}

and the code is valid but the second template parameter isn't defaulted so has to be explicit-ed.

So, almost as in first case, you have

float a{1.0f}, b{2.0f};

SwapInPlace(a, b); // compilation error
SwapInPlace<float, void>(a, b); // compile

The bad part is that a default value doesn't distinguish a function signature; so if you have two SwapInPlace() alternative functions

template< class T, class = std::enable_if_t< std::is_integral_v< T > > >
void SwapInPlace( T& left, T& right )
{...}

template< class T, class = std::enable_if_t< not std::is_integral_v< T > > >
void SwapInPlace( T& left, T& right )
{...}

after the substitutions you have

template< class T, class = void>
void SwapInPlace( T& left, T& right )
{...}

template< class T, class>
void SwapInPlace( T& left, T& right )
{...}

So you have two functions with exactly the same signature (remember: the = void doesn't count). This is unacceptable for C++ rules so you have a compilation error.

Suggestion: avoid the second way because doesn't works when you have to develop alternative functions.


Third case

template< class T >
std::enable_if_t < std::is_integral_v< T > > SwapInPlace( T& left, T& right )
 {...}

This is the simplest case, as far I can understand.

In case T is integral, you get

template< class T >
void SwapInPlace( T& left, T& right )

that is valid code, so the function is enabled.

In case T isn't integral, you loose the return value

template< class T >
     SwapInPlace( T& left, T& right )

so the code isn't valid so the function is disabled.


Fourth case: see the first case

max66
  • 65,235
  • 10
  • 71
  • 111
  • Thanks for details. About case when default template parameter is dropped in case when `T` isn't integral - the code you've provided as example just after `So, as in first case, you have` will compile in both cases - with deducted parameter and parameters provided in a call - as you use ints there. Seems you've wanted to use `double` instead, thou it will not work in both cases - with deducted parameter and parameters provided in a call. Anyway there's some ambiguity in this section, could you please look into it? – Soup Endless Feb 20 '21 at 11:53
  • 1
    @SoupEndless - Good point. Yes: in that case the example needs a non-integral type for values. Thanks; corrected (I hope). – max66 Feb 20 '21 at 12:04
3

You can remove everything about enable_if in the question. It boils down to these three:

void is an invalid anonymous template parameter:

template<class T, void>
void SwapInPlace( T& left, T& right );

A void* as an anonymous template parameter with a default value is ok:

template<class T, void* = nullptr >
void SwapInPlace( T& left, T& right );

void as return type is ok:

template<class T>
void SwapInPlace( T& left, T& right );

If you'd changed the first case to a valid anonymous template parameter without a default value, like an int or void*, it would compile:

template<class T, int>
void SwapInPlace( T& left, T& right );

... until you tried actually using it. You'd then get "couldn't deduce template parameter '<anonymous>'" or similar.

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108