不带变量说明符的模板参数上的SFINAE(enable_if)

SFINAE(enable_if) on template arguments without variable specifier

本文关键字:enable SFINAE if 参数 变量 说明符      更新时间:2023-10-16

这是代码:

#include <iostream>
#include <type_traits>

template <class T>
typename std::enable_if<std::is_integral<T>::value,bool>::type
  is_odd (T i) {return bool(i%2);}
// 2. the second template argument is only valid if T is an integral type:
template < class T,
           class = typename std::enable_if<std::is_integral<T>::value>::type>
bool is_even (T i) {return !bool(i%2);}

int main() {
  short int i = 1;    // code does not compile if type of i is not integral
  std::cout << std::boolalpha;
  std::cout << "i is odd: " << is_odd(i) << std::endl;
  std::cout << "i is even: " << is_even(i) << std::endl;
  return 0;
}

我正在努力学习enable_if的正确用法,我知道如果它被用作返回类型说明符,那就是:编译器将忽略代码。也就是说,函数不会在二进制文件中。

如果它被用于模板参数中,我会感到困惑。根据上面的代码,它说the second template argument is only valid if T is an integral type,但我很困惑第二个论点的目的是什么?

我删除了它,并将其更改为:

template < class T>
bool is_even (T i) {return !bool(i%2);}

它仍然运行良好。有人能告诉我它的真正目的是什么吗?上面也没有变量说明符。

或者,如果我做了类似的事情,它可能只是一个检查器

template < class T,
           class B= typename std::enable_if<std::is_integral<T>::value>::type>

允许我访问代码上的B(可以是真的也可以是假的)?

您正在将它与整型一起使用。它只有在与非积分类型一起使用时才会失败。以下应该无法编译:

float f;
is_even(f);

请注意,使用C++14编译器,您可以编写:

template <class T,
          class = std::enable_if_t<std::is_integral<T>::value>>

然而,您可能希望在这里使用static_assert,因为该函数对于非积分类型可能没有意义,并且它在编译时会给出更好的错误消息。

SFINAE中附加参数的另一个用途是降低对给定函数的偏好。与模板参数较少的模板相比,模板参数较多的模板不太可能被选中。

在这种情况下,enable_if的目的是在推导出的T的模板参数不是整型时导致编译错误。我们可以从cppreference/is_integral中看到,积分类型是整数、字符及其有符号和无符号变体。

对于任何其他类型,你都会得到一个错误,看起来像:

main.cpp:21:32: error: no matching function for call to 'is_odd'
  std::cout << "i is odd: " << is_odd(NotIntegral()) << std::endl;
                               ^~~~~~
main.cpp:6:25: note: candidate template ignored: disabled by 'enable_if' [with T = NotIntegral]
typename std::enable_if<std::is_integral<T>::value,bool>::type

我知道如果它被用作返回类型说明符,编译器将忽略代码

这不是真的。返回类型的计算方式与声明的任何其他部分一样。请参阅为什么我应该在函数签名中避免std::enable_if。std::enable_if的位置选择有其优缺点。

但我很困惑第二个论点的目的是什么?

考虑以下示例,其中有一个名为foo的函数,它需要一些T

template<class T> void foo(T);

您希望限制foo的一个重载,其Tvalue成员等于1,并希望在T::value不等于1时使用另一个重载。如果我们有这个:

template<class T> void foo(T); // overload for T::value == 1
template<class T> void foo(T); // overload for T::value != 1

这是如何向编译器传达您希望对两个独立的事物使用两个独立重载的?事实并非如此。它们都是不明确的函数调用:

template<std::size_t N>
struct Widget : std::integral_constant<std::size_t, N> { };
int main() {
    Widget<1> w1;
    Widget<2> w2;
    foo(w1); // Error! Ambiguous!
    foo(w2); // Error! Ambiguous!
}

您需要使用SFINAE根据我们的条件拒绝模板:

template<class T, class = std::enable_if_t<T::value == 1>* = nullptr>
void foo(T); // #1
template<class T, class = std::enable_if_t<T::value != 1>* = nullptr>
void foo(T); // #2

现在正确的被称为:

foo(w1); // OK! Chooses #1
foo(w2); // OK! Chooses #2

std::enable_if的构建方式使它发挥了作用。如果其第一个参数中的条件为true,则它将提供一个名为type的成员typedef,该成员与第二个参数(默认为void)相等。否则,如果条件为false,则不提供条件。一个合理的实施方式可以是:

 template<bool, class R = void>
 struct enable_if { using type = R; };
 template<class R>
 struct enable_if<false, R> { /* empty */ };

因此,如果条件失败,我们将尝试访问一个不存在的::type成员(请记住,std::enable_if_tstd::enable_if<...>::type的别名)。代码的格式可能不正确,但在重载解析过程中,模板只是从候选集中被拒绝,而不是导致硬错误,而是使用其他重载(如果存在)。

这是对SFINAE的简单解释。有关详细信息,请参阅cppreference页面。

在您的情况下,is_oddis_even只有一个过载,因此如果您将NotIntegral传递给它们,它们将失败,因为它们各自的过载已从候选集中删除,并且由于没有更多的过载来评估过载,因此解决方案会因上述错误而失败。这可以用static_assert消息稍微清理一下。

template<class T>
bool is_even (T i) {
    static_assert(std::is_integral<T>::value, "T must be an integral type");
    return !bool(i%2);
}

现在,您将不再收到一条看起来很奇怪的错误消息,而是收到您自己的自定义消息。这也排除了SFINAE,因为static_assert是在模板参数被替换后计算的,所以您可以选择走哪条路。我推荐这个断言,因为它更干净,这里不需要SFINAE。