警告:这是一个非常长的解释,但希望它不仅能真正解释 SFINAE 的作用,还能让您了解何时以及为何使用它。
好的,为了解释这一点,我们可能需要备份并解释一下模板。众所周知,Python 使用的是通常所说的鸭式类型——例如,当您调用一个函数时,您可以将一个对象 X 传递给该函数,只要 X 提供该函数使用的所有操作即可。
在 C++ 中,普通(非模板)函数要求您指定参数的类型。如果你定义了这样的函数:
int plus1(int x) { return x + 1; }
您可以仅将该函数应用于int。事实上,它使用x 的方式可以同样适用于long 或float 等其他类型,这没什么区别——它只适用于int。
为了更接近 Python 的鸭子类型,您可以创建一个模板:
template <class T>
T plus1(T x) { return x + 1; }
现在我们的plus1 更像是在Python 中的情况——特别是,我们可以同样很好地调用它来定义x + 1 的任何类型的对象x。
现在,考虑一下,例如,我们想要将一些对象写到流中。不幸的是,其中一些对象使用stream << object 写入流,而其他对象则使用object.write(stream);。我们希望能够处理其中任何一个,而无需用户指定哪个。现在,模板专门化允许我们编写专门的模板,所以如果它是使用object.write(stream) 语法的 one 类型,我们可以这样做:
template <class T>
std::ostream &write_object(T object, std::ostream &os) {
return os << object;
}
template <>
std::ostream &write_object(special_object object, std::ostream &os) {
return object.write(os);
}
这对一种类型来说很好,如果我们想要做得足够糟糕,我们可以为所有不支持stream << object的类型添加更多特化——但尽快(例如)用户添加了一个不支持 stream << object 的新类型,事情又中断了。
我们想要的是一种将第一个特化用于任何支持stream << object; 的对象的方法,而将第二个特化用于其他任何对象(尽管有时我们可能希望为使用x.print(stream); 的对象添加第三个特化)。
我们可以使用 SFINAE 来做出决定。为此,我们通常依赖于 C++ 的其他一些古怪的细节。一种是使用sizeof 运算符。 sizeof 确定类型或表达式的大小,但它完全是在编译时通过查看所涉及的 types 来完成的,而不评估表达式本身。例如,如果我有类似的东西:
int func() { return -1; }
我可以使用sizeof(func())。在这种情况下,func() 返回一个int,所以sizeof(func()) 等价于sizeof(int)。
第二个经常使用的有趣项目是数组的大小必须为正数,而不是为零。
现在,把它们放在一起,我们可以做这样的事情:
// stolen, more or less intact from:
// http://stackoverflow.com/questions/2127693/sfinae-sizeof-detect-if-expression-compiles
template<class T> T& ref();
template<class T> T val();
template<class T>
struct has_inserter
{
template<class U>
static char test(char(*)[sizeof(ref<std::ostream>() << val<U>())]);
template<class U>
static long test(...);
enum { value = 1 == sizeof test<T>(0) };
typedef boost::integral_constant<bool, value> type;
};
这里我们有两个test 的重载。其中第二个采用变量参数列表(...),这意味着它可以匹配任何类型——但它也是编译器在选择重载时做出的最后选择,所以它只会 如果第一个 not 匹配,则匹配。 test 的另一个重载有点有趣:它定义了一个接受一个参数的函数:一个指向返回 char 的函数的指针数组,其中数组的大小(本质上)是sizeof(stream << object)。如果stream << object 不是一个有效的表达式,sizeof 将产生 0,这意味着我们创建了一个大小为零的数组,这是不允许的。这就是 SFINAE 本身出现的地方。尝试用不支持operator<< 的类型替换U 会失败,因为它会生成一个大小为零的数组。但是,这不是错误——它只是意味着从重载集中消除了该函数。因此,在这种情况下,只能使用另一个函数。
然后在下面的enum 表达式中使用它——它查看来自test 的选定重载的返回值并检查它是否等于1(如果是,则意味着函数返回char被选中,否则,返回long的函数被选中)。
结果是 has_inserter<type>::value 将是 l 如果我们可以使用 some_ostream << object; 将编译,而 0 如果它不会。然后,我们可以使用该值来控制模板特化,以选择正确的方式来写出特定类型的值。