C++ 中的 SFINAE 是什么?
您能否用不懂 C++ 的程序员可以理解的语言解释一下?另外,SFINAE 对应于 Python 这样的语言中的什么概念?
C++ 中的 SFINAE 是什么?
您能否用不懂 C++ 的程序员可以理解的语言解释一下?另外,SFINAE 对应于 Python 这样的语言中的什么概念?
警告:这是一个很长的解释,但希望它不仅能真正解释 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
任何类型的对象。x + 1
现在,例如,考虑我们想要将一些对象写入流。不幸的是,其中一些对象使用 写入流stream << object
,而其他对象则使用object.write(stream);
。我们希望能够处理其中任何一个,而无需用户指定哪个。现在,模板专门化允许我们编写专门的模板,所以如果它是一种使用object.write(stream)
语法的类型,我们可以这样做:
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
确定类型或表达式的大小,但它完全是在编译时通过查看所涉及的类型来完成的,而不评估表达式本身。例如,如果我有类似的东西:
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
. 其中第二个采用变量参数列表(the ...
),这意味着它可以匹配任何类型——但它也是编译器在选择重载时做出的最后选择,因此只有在第一个不匹配时才会匹配。的另一个重载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
如果您有一些重载的模板函数,则在执行模板替换时,一些可能使用的候选对象可能无法编译,因为被替换的东西可能没有正确的行为。这不被认为是编程错误,失败的模板只是从可用于该特定参数的集合中删除。
我不知道 Python 是否有类似的特性,也不明白为什么非 C++ 程序员应该关心这个特性。但是如果你想了解更多关于模板的知识,最好的书是C++ Templates: The Complete Guide。
SFINAE 是 C++ 编译器用于在重载解析期间过滤掉一些模板化函数重载的原则 (1)
当编译器解析一个特定的函数调用时,它会考虑一组可用的函数和函数模板声明来找出将使用哪一个。基本上,有两种机制可以做到这一点。一可以被描述为句法。给定声明:
template <class T> void f(T); //1
template <class T> void f(T*); //2
template <class T> void f(std::complex<T>); //3
resolvef((int)1)
将删除版本 2 和 3,因为int
is not equal to complex<T>
or T*
for some T
。同样,f(std::complex<float>(1))
将删除第二个变体并f((int*)&x)
删除第三个。编译器通过尝试从函数参数中推断出模板参数来做到这一点。如果扣除失败(如T*
反对int
),则丢弃重载。
我们想要这样做的原因很明显 - 我们可能希望对不同类型做一些稍微不同的事情(例如,复数的绝对值由计算x*conj(x)
并产生一个实数,而不是复数,这与浮点数的计算不同)。
如果你之前做过一些声明式编程,这个机制类似于(Haskell):
f Complex x y = ...
f _ = ...
C++ 更进一步的方式是,即使推导的类型是正确的,推导也可能会失败,但是将替换回另一个会产生一些“荒谬”的结果(稍后会详细介绍)。例如:
template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);
推导时f('c')
(我们使用单个参数调用,因为第二个参数是隐式的):
T
其char
产生的结果T
为char
T
将声明中的所有 s 替换为char
s。这产生void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
.int [sizeof(char)-sizeof(int)]
。这个数组的大小可以是例如。-3(取决于您的平台)。<= 0
无效,因此编译器会丢弃重载。替换失败不是错误,编译器不会拒绝程序。最后,如果存在多个函数重载,编译器会使用转换序列比较和模板的部分排序来选择“最佳”的一个。
还有更多像这样工作的“荒谬”结果,它们在标准(C ++ 03)的列表中枚举。在 C++0x 中,SFINAE 的领域扩展到几乎任何类型错误。
我不会写一个广泛的 SFINAE 错误列表,但一些最受欢迎的是:
typename T::type
forT = int
或T = A
whereA
是一个没有嵌套类型的类,称为type
.int C::*
为了C = int
这种机制与我所知道的其他编程语言中的任何东西都不相似。如果你要在 Haskell 中做类似的事情,你会使用更强大的守卫,但在 C++ 中是不可能的。
1:或在谈论类模板时部分模板特化
Python 根本帮不了你。但是您确实说您已经基本熟悉模板。
最基本的 SFINAE 结构是使用enable_if
. 唯一棘手的部分是它class enable_if
没有封装SFINAE,它只是公开它。
template< bool enable >
class enable_if { }; // enable_if contains nothing…
template<>
class enable_if< true > { // … unless argument is true…
public:
typedef void type; // … in which case there is a dummy definition
};
template< bool b > // if "b" is true,
typename enable_if< b >::type function() {} //the dummy exists: success
template< bool b >
typename enable_if< ! b >::type function() {} // dummy does not exist: failure
/* But Substitution Failure Is Not An Error!
So, first definition is used and second, although redundant and
nonsensical, is quietly ignored. */
int main() {
function< true >();
}
在 SFINAE 中,有一些结构会设置错误条件(class enable_if
此处)和许多并行的,否则会相互冲突的定义。除了一个定义,编译器选择并使用它而不抱怨其他的定义中都出现了一些错误。
什么样的错误可以接受是最近才标准化的一个主要细节,但你似乎没有问这个问题。
Python 中没有任何东西与 SFINAE 很相似。Python 没有模板,当然也没有解析模板特化时发生的基于参数的函数解析。在 Python 中,函数查找纯粹是按名称完成的。