问题描述
这是一个关于当模板参数用作模板参数 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
您已经从 T
和 int
推导出 a
为 b
,但编译器无法确定第二个(未命名)模板参数的值.
因此,要进行有效调用,您必须显式调用函数的第二个模板参数
SwapInPlace<int,0>(a,b); // OK
调用以这种方式工作,因为没有发生推导并明确说明 T
是 int
并且 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
可从 left
和 right
参数推导出,第二个未命名的参数是默认值,因此无需显式
但是当 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 更改为有效的匿名模板参数没有默认值,例如 int
或 void*
,它将编译:
template<class T,T& right );
... 直到您尝试实际使用它为止。然后您会得到“无法推断模板参数'<anonymous>
'”或类似信息。