如何将模板参数从类型更改为非类型使 SFINAE 工作

How does changing a template argument from a type to a non-type make SFINAE work?

本文关键字:类型 工作 SFINAE 参数      更新时间:2023-10-16

摘自std::enable_ifcppreference.com 文章,

注释
一个常见的错误是声明两个函数模板,这两个模板仅在其默认模板参数上有所不同。这是非法的,因为默认模板参数不是函数模板签名的一部分,并且声明具有相同签名的两个不同的函数模板是非法的。

/*** WRONG ***/
struct T {
enum { int_t,float_t } m_type;
template <
typename Integer,
typename = std::enable_if_t<std::is_integral<Integer>::value>
>
T(Integer) : m_type(int_t) {}
template <
typename Floating,
typename = std::enable_if_t<std::is_floating_point<Floating>::value>
>
T(Floating) : m_type(float_t) {} // error: cannot overload
};
/* RIGHT */
struct T {
enum { int_t,float_t } m_type;
template <
typename Integer,
typename std::enable_if_t<std::is_integral<Integer>::value, int> = 0
>
T(Integer) : m_type(int_t) {}
template <
typename Floating,
typename std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0
>
T(Floating) : m_type(float_t) {} // OK
};

 

我很难理解为什么*** WRONG ***版本不编译而*** RIGHT***版本可以编译。解释和例子对我来说是货物崇拜。上面所做的只是将类型模板参数更改为非类型模板参数。对我来说,这两个版本都应该有效,因为两者都依赖于std::enable_if<boolean_expression,T>具有一个名为type的 typedef 成员,而std::enable_if<false,T>没有这样的成员。替换失败(不是错误)应该会导致两个版本。

看看标准,它说在[温度扣除]中

引用函数模板专用化时,所有模板参数都应具有值

后来那

如果模板参数尚未

推导,并且其对应的模板参数具有默认参数,则通过将前面模板参数确定的模板参数替换为默认参数来确定模板参数。如果替换导致无效类型(如上所述),则类型推断将失败。

这种类型推理失败不一定是错误,这就是SFINAE的全部内容。

为什么将*** WRONG ***版本中的 typename 模板参数更改为非 typename 参数会使*** RIGHT ***版本"正确"?

主要是因为 [temp.over.link]/6 没有谈论模板默认参数:

如果两个模板头的模板参数列表具有相同的长度,则两个模板头是等效的,相应的

模板参数是等效的,如果其中一个具有 requires 子句,则它们都具有requires 子句并且相应的约束表达式是等效的。在以下条件下,两个模板参数是等效的:

  • 它们声明相同类型的模板参数,

  • 如果其中任何一个声明了模板参数包,它们都声明了,

  • 如果它们声明非类型模板参数,则它们具有等效的类型,

  • 如果它们声明模板模板参数,则其模板参数是等效的,并且

  • 如果其中任何一个是用限定概念名称声明的,则它们都是,并且限定概念名称是等效的。

然后通过 [temp.over.link]/7:

如果两个函数模板在同一作用域中声明,具有相同的名称,具有等效的模板头,并且具有等效的返回类型、参数列表和尾随要求子句(如果有),则使用上述规则来比较涉及模板参数的表达式。

。第一个示例中的两个模板是等效的,而第二个示例中的两个模板则不是。因此,第一个示例中的两个模板声明相同的实体,并导致 [class.mem]/5 的构造格式不正确:

一个成员不得在成员规范中声明两次,...

改写 cppreference 引用,在错误的情况下,我们有:

typename = std::enable_if_t<std::is_integral<Integer>::value>
typename = std::enable_if_t<std::is_floating_point<Floating>::value>

它们都是默认模板参数,并且不是函数模板签名的一部分。因此,在错误的情况下,您会想出两个相同的签名。

在正确的情况下:

typename std::enable_if_t<std::is_integral<Integer>::value, int> = 0

typename std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0

您不再有默认模板参数,而是具有默认值(=0) 的两种不同类型。因此签名是不同的


评论更新:澄清差异,

具有默认类型的模板参数的示例:

template<typename T=int>
void foo() {};
// usage
foo<double>();
foo<>();

具有默认值的非类型模板参数的示例

template<int = 0>
void foo() {};
// usage
foo<4>();
foo<>();

在您的示例中可能令人困惑的最后一件事是enable_if_t的使用,实际上在您的正确情况下代码中,您有一个多余的typename

template <
typename Integer,
typename std::enable_if_t<std::is_integral<Integer>::value, int> = 0
>
T(Integer) : m_type(int_t) {}

最好写成:

template <
typename Floating,
std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0
>

(第二项声明也是如此)。

这正是enable_if_t的作用:

template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type;

不必添加typename(与旧enable_if相比)

第一个版本是错误的,就像这个片段是错误的一样:

template<int=7>
void f();
template<int=8>
void f();

原因与替换失败无关:替换仅在使用函数模板时发生(例如在函数调用中),但仅声明就足以触发编译错误。

相关的标准措辞是 [dcl.fct.default]:

默认参数应仅在 [...] 或模板参数 ([temp.param]) 中指定;[...]

默认参数不应由后面的声明重新定义(甚至不能重新定义相同的值)。

第二个版本是正确的,因为函数模板具有不同的签名,因此编译器不会将其视为同一实体。

让我们尝试省略默认参数值和不同的名称(请记住:默认模板参数不是函数模板签名的一部分,就像参数名称一样),看看"错误"模板函数签名的样子:

template
<
typename FirstParamName
,    typename SecondParamName
>
T(FirstParamName)
template
<
typename FirstParamName
,   typename SecondParamName
>
T(FirstParamName)

哇,它们一模一样!所以T(Floating)实际上是对T(Integer)的重新定义 而右版本声明了两个具有不同参数的模板:

template
<
typename FirstParamName
,    std::enable_if_t<std::is_integral<FirstParamName>::value, int> SecondParamName
> 
T(FirstParamName)
template
<
typename FirstParamName
,   std::enable_if_t<std::is_floating_point<FirstParamName>::value, int> SecondParamName
>
T(FirstParamName)

另请注意,在"Right"模板声明中,无需在std::enable_if_t<std::is_floating_point<Floating>::value, int>之前使用typename,因为那里没有依赖类型名称。

这与类型或非类型无关

关键是:它是否通过了两阶段查找的第一步。

为什么?因为 SFINAE 在查找的第二阶段工作,所以当模板被调用时(如@cpplearner所说)

所以:

这不起作用(案例 1):

template <
typename Integer,
typename = std::enable_if_t<std::is_integral<Integer>::value>
>

这项工作以及您的非类型案例(案例 2):

template <
typename Integer,
typename = std::enable_if_t<std::is_integral<Integer>::value>,  
typename = void
>

在案例一中,编译器看到:相同的名称,相同数量的模板参数和参数不依赖于模板,相同的 arugments => 这是一回事 => 错误

在案例二中,参数数量不同,好吧,让我们看看它以后是否有效=> SFINAE => 好的

在你正确的情况下:编译器看到:相同的名称,相同数量的模板参数和参数是模板依赖的(具有默认值,但他现在不在乎)=>让我们看看什么时候调用=> SFINAE => OK

顺便问一下,你如何调用构造函数?

从这篇文章

无法为构造函数显式指定模板,因为无法命名构造函数。

而你真的不能:

T t =T::T<int,void>(1);

错误:无法直接调用构造函数"T::T" [-允许]

您仍然可以使其与专业化和SFINAE一起使用:

#include <iostream>
#include <type_traits>
using namespace std;

template <
typename Type,
typename = void
>
struct T {
};
template < typename Type>
struct T<
Type,
std::enable_if_t<std::is_integral<Type>::value>
>  {
float m_type;
T(Type t) : m_type(t) { cout << __PRETTY_FUNCTION__ << endl; }
};
template < typename Type>
struct T<
Type,
std::enable_if_t<std::is_floating_point<Type>::value>
>  {
int m_type;
T(Type t) : m_type(t) { cout << __PRETTY_FUNCTION__ << endl; }
};
int main(){
T<int> t(1); // T<Type, typename std::enable_if<std::is_integral<_Tp>::value, void>::type>::T(Type) [with Type = int; typename std::enable_if<std::is_integral<_Tp>::value, void>::type = void]
cout << endl;
T<float> t2(1.f);// T<Type, typename std::enable_if<std::is_floating_point<_Tp>::value, void>::type>::T(Type) [with Type = float; typename std::enable_if<std::is_floating_point<_Tp>::value, void>::type = void]
return 0;
}

这是 C++14 风格,在 17 中,您可以想出一个只需T t(1)即可编译的版本,但我不是类模板参数推导的专家

我将对错误的版本进行小幅重写,以帮助讨论正在发生的事情。

struct T {
enum { int_t,float_t } m_type;
template <
typename Integer,
typename U = std::enable_if_t<std::is_integral<Integer>::value>
>
T(Integer) : m_type(int_t) {}
template <
typename Floating,
typename U = std::enable_if_t<std::is_floating_point<Floating>::value>
>
T(Floating) : m_type(float_t) {} // error: cannot overload
};

我所做的只是给以前匿名的第二个参数一个名字——U

第一个版本不起作用的原因是,在您显式提供第二个参数的情况下,无法在两者之间做出决定。例如1

f<int,void>(1);

这应该推导出哪个功能?如果是整数版本,它当然可以工作 - 但是浮点版本呢?好吧,它有T = int但是U呢?好吧,我们刚刚给它一个类型,bool,所以我们有U = bool.因此,在这种情况下无法在两者之间做出决定,它们是相同的。(请注意,在整数版本中,我们仍然有U = bool)。

因此,如果我们显式命名第二个模板参数,则推导失败。那又怎样?在实际用例中,这不应该发生。我们将使用类似的东西

f(1.f);

可以扣除的地方。好吧,您会注意到,即使没有声明,编译器也会给您一个错误。这意味着它已经决定在给它一个要推断的类型之前就无法推断,因为它已经检测到我上面指出的问题。好吧,从 intro.defs 我们有签名为

⟨类成员函数模板⟩名称、参数类型列表、函数所属的类、cv 限定符(如果有)、引用限定符(如果有)、返回类型(如果有)和模板参数列表

从 temp.over.link 我们知道两个模板函数定义不能具有相同的签名。

不幸的是,该标准似乎对"模板参数列表"的确切含义相当模糊。我搜索了该标准的几个不同版本,但没有一个给出我能找到的明确定义。如果具有不同默认值的类型参数构成唯一参数,则不清楚"模板参数列表"是否相同。鉴于我要说这实际上是未定义的行为,并且编译器错误是处理该问题的可接受方法。

判决还没有出来,如果有人能在"模板参数列表"的标准中找到明确的定义,我很乐意添加它以获得更令人满意的答案。

编辑:

正如xskxkr所指出的,最新的草案实际上给出了更具体的定义。模板有一个模板,其中包含一个模板参数列表,它是一系列模板参数它不包括定义中的默认参数。因此,根据当前的草案,拥有两个相同但具有不同默认参数的模板无疑是错误的,但是您可以通过使第二个参数的类型取决于enable_if的结果来"愚弄"它,使其认为您有两个单独的模板头


1作为旁注,我找不到一种方法来显式实例化非模板类的模板构造函数。这是一个奇怪的结构。我在示例中使用了f,因为我实际上可以让它与免费函数一起使用。也许其他人可以弄清楚语法?