函数模板参数推导模板参数 vs 默认模板参数 vs 返回类型

问题描述

这是一个关于当模板参数用作模板参数 vs 作为认模板参数 vs 作为返回类型时模板推导如何工作的问题。

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: 错误 C2783: 'void SwapInPlace(T &,T &)': 无法推断 '__formal' 的模板参数

GCC:无法推导出模板参数 -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:返回类型

在返回类型的情况下,一切都可以编译并运行良好:

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,std::enable_if_t< std::is_floating_point_v< T > >* = nullptr >
void SwapInPlace( T& left,T& right )
{...}

所以问题是:在这 4 种情况下如何演绎:为什么它在第一种情况下失败而在其他三种情况下成功?

解决方法

演绎在这四种情况下是如何工作的:为什么它在第一种情况下失败而在其他三种情况下成功?


第一种情况

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

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

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

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

调用以这种方式工作,因为没有发生推导并明确说明 Tint 并且 int 参数是 0

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

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

// .........................................................VVVVVV.VVVV
template< class 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); // 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,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,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 不是整数,您将失去返回值

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

因此代码无效,因此该功能被禁用。


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

,

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

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

template<class T,T& right );

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

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

void 作为返回类型是可以的:

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

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

template<class T,T& right );

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