SFINAE:这里发生了什么?

13

我目前正在尝试理解一段C++代码,并遇到了SFINAE结构(对我来说是新的)。 我已经基于我正在查看的代码创建了一个最小示例,如下所示:

#include<iostream>

/* ----------------------------------------------
 Define two kernels: characterized by their dimension
   ---------------------------------------------- */
struct Kern2
{
  static constexpr int dim = 2;
};

struct Kern3
{
  static constexpr int dim = 3;
};

/* ----------------------------------------------
 Choose which function to evaluate based on 
 dimension of Kern (Kern::dim)
   ---------------------------------------------- */
template<class Kern,
         typename std::enable_if<Kern::dim == 2, bool>::type = true>
inline void apply_kern(){
  std::cout << "dim=2" << "\n";
}

template<class Kern,
         typename std::enable_if<Kern::dim == 3, bool>::type = false>
inline void apply_kern(){
  std::cout << "dim=3" << "\n";
}

// Try to see if the above SFINAE construct works!
int main()
{

 apply_kern<Kern2>(); // should print 'dim=2'
 apply_kern<Kern3>(); // should print 'dim=3'

  return 0;
}

这将输出:

> dim=2
> dim=3

这正是它应该做的事情。然而,我无法准确理解 它是如何工作的?特别是,如果我切换

标签,似乎会创建相同的输出。

typename std::enable_if<Kern::dim == 2, bool>::type = true

到以下行:

typename std::enable_if<Kern::dim == 2, bool>::type = false

所以我想知道这些的意思是什么?如果有人能够友好地解释一下正在发生什么,我将不胜感激!不幸的是,我在网上找不到这种使用SFINAE的确切方式。

谢谢!


1
经过上述修改,您的代码不应该编译。 - geza
1
@TedLyngmo:我错过了什么吗?https://godbolt.org/z/wbvn7I - geza
3
=true=false并不是比较运算符,它们只是模板参数的默认值,如果省略了模板参数则会使用这些默认值。SFINAE魔法是在std::enable_if内部完成的,而不是在=true/false中完成的。 - Werner Henze
3
这篇文章是由Eli Bendersky撰写的介绍,我最近参考了它:SFINAE 和 enable_if - Scheff's Cat
@TedLyngmo:我们中的一员 :) 是我。抱歉。 - geza
显示剩余12条评论
2个回答

16
typename std::enable_if<Kern::dim == 2, bool>::type = true>

That says:

typename:

以下术语定义了一种类型。
std::enable_if<Kern::dim == 2, bool>

如果第一个参数的条件为真,那么此模板将定义第二个模板参数的一种类型。所以在这里,如果dimm == 2为真,则模板std::enable_if提供了一个可以通过::type访问的bool类型。

如果条件为真,则术语:

typename std::enable_if<Kern::dim == 3, bool>::type

变得非常简单:
bool

现在你需要在它后面添加= true。你是否在任何地方使用了bool值?没有!因此这根本不重要!你也可以写成:

typename std::enable_if<Kern::dim == 3, int>::type = 42

这将导致与您在此处定义的值无关!
您检查的条件在Kern :: dim == 3中。这个条件必须为true或false。
如果条件评估为false,则模板enable_if不包含type并且表达式失败。这时SFINAE就发挥作用了。这种失败不会是一个错误,但会使模板定义“不可见”,因为由于失败而“不能”使用。
对于评论中的附加问题的补充:
当然,您可以为布尔模板默认参数添加名称,并在下面的代码中使用它,如下所示:
template<class Kern,
         typename std::enable_if<Kern::dim == 2, bool>::type myVal = true>
inline void apply_kern(){
  std::cout << "dim=2" << "\n";
  std::cout << "bool val: " << myVal << std::endl;
}

顺便说一句: 我们经常看到SFINAE被用在一些情况下,而一个简单的模板重载能够起到同样的作用。常常情况下,模板重载更容易理解(这里可能不是这样的:-))。我只是给个提示:先检查是否真正需要使用SFINAE,考虑使用重载来代替。

使用模板重载代替SFINAE:

/* ----------------------------------------------
   Define two kernels: characterized by their dimension
   ---------------------------------------------- */
struct Kern2 { static constexpr int dim = 2; };
struct Kern3 { static constexpr int dim = 3; };

/* ----------------------------------------------
   Choose which function to evaluate based on 
   dimension of Kern (Kern::dim)
   ---------------------------------------------- */
template < int x > inline void apply_kern_impl();

template<>
inline void apply_kern_impl<2>() { std::cout << "dim=2" << "\n"; }

template<>
inline void apply_kern_impl<3>() { std::cout << "dim=3" << "\n"; }

template< typename T>
inline void apply_kern() { apply_kern_impl<T::dim>(); }

int main()
{
    apply_kern<Kern2>(); // should print 'dim=2'
    apply_kern<Kern3>(); // should print 'dim=3'

    return 0;
}

2
很好的开始。现在我们需要解释一下当条件为假时它是如何工作的 :) - Lightness Races in Orbit
1
@Sam 是的,你只需要在 ::type= 之间放一个名称。然而,在函数中你不一定要使用它(它只是用于 SFINAE),并且默认值意味着用户不必编写 apply_kern<Kern2, true>();(或 false,这没有关系),而只需编写 apply_kern<Kern2>() - Max Langhof
@Klaus:“默认值意味着用户不必编写apply_kern<Kern2, true>();(或false,无关紧要),而只需编写apply_kern<Kern2>()” - 很好的解释!正是我需要的。感谢您提供这些细节。 :) - Sam
2
这现在是一个非常出色的答案。 - Lightness Races in Orbit
2
这个答案教会了我更多关于enable_if的知识,比我花费数小时阅读书籍和在网上搜索明确示例所学到的还要多。 - pipe
显示剩余2条评论

1

std::enable_if<bool expression, return type of function>::type 告诉编译器在编译时,如果 bool 表达式为 true,则编译此内容。因此,当您在 main() 中调用 apply_kern<Kern2>() 时,编译器进入第一个 std::enable_if,因为您的 Kern::dim 确实为 2。例如,如果您没有调用 apply_kern<Kern3>(),则编译器会注意到第二个 std::enable_if 为 false,其内部范围不会被编译。这就像是一个 if 语句,在编译时进行判断。如果您觉得这种语法很奇怪,您也可以使用具有两个模板别名的模板函数来获得完全相同的结果,分别为 Kern2Kern3

对于最后一个问题,我建议使用 typename std::enable_if<!(Kern::dim == 2), bool>::type inline void apply_kernel(){...} 来获得相同的结果。


@LeoStar:感谢您的回答!所以关键是要向编译器提供一些表达式,除非我们处于期望的情况下,否则它将失败?是这样吗?特别地,我正在查看的代码中使用的特定结构的使用没有更深层次的含义吗? - Sam
1
@Sam 正确。在那里选择的类型和默认值是无关紧要的,它们仍然未使用。整个目的是在条件不满足时提供 没有 类型,这导致替换失败(因此忽略该模板)。默认值是为用户方便而设置的(如果您删除默认值,一切仍然有效,用户只需在调用 apply_kern 时向模板参数列表添加一个无意义的值即可)。 - Max Langhof
@Sam,实际上你只需要编写你想让编译器编译的表达式。在其他情况下,在编译时std::enable_if内部的bool表达式将返回false,这样函数就不会被编译。我认为它就像enable_if的名称:编写所有你想让编译器编译的情况。这只是一种实现方式,但是通过正确的bool表达式在enable_if内部,可以实现这一点。如果在std::enable_if内部添加额外的检查,例如std::is_same,则对于模板元编程来说具有更深层次的意义。 - LeoSar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接