模板参数与默认模板参数与 return 类型的函数模板参数推导

function template parameter deduction of template parameter vs of default template parameter vs of return type

这是一个关于当模板参数用作模板参数与默认模板参数与 return 类型时模板推导如何工作的问题。

1: 普通模板参数

经测试,使用 GCC 和 VS 编译以下代码片段无法推断出由 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: 默认模板参数

将第二个模板参数声明为默认模板参数可以使推导工作正常:

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 )
{...}

向第二次重载添加 bool = true 只是为了避免编译错误,因为无法根据默认模板参数重载函数模板。只想关注演绎在这里可以正常工作的事实。如果只有一个模板使用默认参数,例如对于 std::is_integral,它会在我们向其传递正确参数的条件下编译并正常工作。

3: return 类型

在 return 的情况下,键入所有内容都可以编译并运行良好:

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: 具有默认值的模板参数

使此代码编译的另一种方法是为 std::enable_if 定义的模板参数添加默认值。在最后一个右尖括号之前添加 * = nullptr,因此如果 std::enable_if 条件计算为 true 那么我们的第二个参数变为 void*,默认值为 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 )
{...}

所以问题是:推论在这 4 种情况下是如何工作的:为什么第一种情况失败而其他三种情况成功?

您可以删除问题中有关 enable_if 的所有内容。归结为这三个:

void 是无效的匿名模板参数:

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

A void* 作为具有默认值的匿名模板参数是可以的:

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

void 因为 return 类型可以:

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

如果您将第一种情况更改为有效的匿名模板参数 而没有 默认值,例如 intvoid*,它将编译:

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

...直到您尝试实际使用它。然后你会得到“无法推断模板参数'<anonymous>'”或类似的。

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


第一个案例

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

假设std::is_integral_v为真;使用 std::enable_if_t 替换你得到

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

这不是有效的 C++ 代码,因为它要求 void 值(用于第二个模板参数)但 void 不能有有效值。

假设您用 int(接受有效值的类型)替换第二个模板参数的类型

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

在积分的情况下 T 你得到的值

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

这是有效的但是...假设调用是

int a{1}, b{2};

SwapInPlace(a, b); // compilation error

你已经从 ab 推导出 Tint 但编译器无法确定第二个(未命名)的值模板参数。

因此,要进行有效调用,您必须明确调用函数的第二个模板参数

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

这种调用方式是有效的,因为没有推导发生并明确表示 Tint 并且 int 参数是 0.

这可行但不舒服,因为您必须明确可以推导的 T

为避免此问题,您可以为第二个模板参数添加一个默认值

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

所以,在 T 积分的情况下,你得到

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

现在简单的调用

int a{1}, b{2};

SwapInPlace(a, b); // OK now

有效,因为 T 被推断为 int 并且第二个模板参数默认为零。

有效,但您可以观察到您现在处于第四种情况(使用 int 而不是 void *0 而不是 nullptr


第二种情况

第二种情况是个坏主意。

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

T积分时成为

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

这是一个有效的代码,T 可以从 leftright 参数中推导出来,第二个未命名的参数是默认的,所以没有必要明确它。

但是当 T 不是整数时,SFINAE 失败仅丢弃第二个模板参数的默认值,因此您得到

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

并且代码有效,但第二个模板参数不是默认的,因此必须是显式的。

所以,几乎和第一种情况一样,你有

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

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

不好的部分是默认值不区分函数签名;所以如果你有两个SwapInPlace()替代函数

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 )
{...}

换人后

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

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

所以您有两个具有完全相同签名的函数(请记住:= void 不算在内)。这对于 C++ 规则是不可接受的,所以你有一个编译错误。

建议:避免第二种方式,因为当你必须开发替代功能时,这种方式行不通。


第三种情况

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

这是我能理解的最简单的情况。

如果 T 是整数,你得到

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

这是有效代码,因此该功能已启用。

如果 T 不是整数,您将丢失 return 值

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

所以代码无效,所以该功能被禁用。


第四种情况:见第一种情况