解释一下 C++ SFINAE 到非 C++程序员
C++ 中的 SFINAE 是什么?
您能用不懂 C++ 的程序员可以理解的语言解释一下吗?另外,SFINAE 对应于 Python 这样的语言中的什么概念?
What is SFINAE in C++?
Can you please explain it in words understandable to a programmer who is not versed in C++? Also, what concept in a language like Python does SFINAE correspond to?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
警告:这是一个非常的解释,但希望它不仅真正解释了 SFINAE 的作用,而且还给出了您何时以及为何使用它的一些想法。
好的,为了解释这一点,我们可能需要稍微备份和解释一下模板。众所周知,Python 使用通常所说的鸭子类型——例如,当你调用一个函数时,你可以将一个对象 X 传递给该函数,只要 X 提供该函数使用的所有操作。
在 C++ 中,普通(非模板)函数要求您指定参数的类型。如果您定义了如下函数:
您只能将该函数应用于
int
。事实上,它使用x
的方式可以同样适用于其他类型,例如long
或float
没有什么区别——它只适用于int
。为了获得更接近 Python 的鸭子类型,您可以创建一个模板:
现在我们的
plus1
更像是在 Python 中——特别是,我们可以同样好地调用它到一个对象x
为定义了x + 1
的任何类型。现在,例如,考虑我们想要将一些对象写入流。不幸的是,其中一些对象是使用
stream << 写入流的。 object
,但其他人使用object.write(stream);
代替。我们希望能够处理其中任何一个,而无需用户指定哪一个。现在,模板专业化允许我们编写专门的模板,因此如果它是使用object.write(stream)
语法的一个类型,我们可以这样做:对于一种类型来说很好,如果我们非常想要,我们可以为所有不支持
stream <<的类型添加更多专门化。 object
——但是一旦(例如)用户添加了不支持stream << 的新类型object
,事情再次崩溃。我们想要的是一种对任何支持
stream << 的对象使用第一个专门化的方法。 object;
,但第二个用于其他任何东西(尽管我们有时可能想为使用x.print(stream);
的对象添加第三个)。我们可以使用 SFINAE 来做出决定。为此,我们通常依赖 C++ 的其他一些奇怪的细节。一种是使用
sizeof
运算符。sizeof
确定类型或表达式的大小,但它完全在编译时通过查看所涉及的类型来确定,而不评估表达式本身。例如,如果我有类似的内容:我可以使用
sizeof(func())
。在本例中,func()
返回一个int
,因此sizeof(func())
相当于sizeof(int)< /代码>。
经常使用的第二个有趣的事实是数组的大小必须为正数,不为零。
现在,将它们放在一起,我们可以做这样的事情:
这里我们有两个
test
重载。其中第二个采用可变参数列表(...
),这意味着它可以匹配任何类型 - 但这也是编译器在选择重载时做出的最后选择,因此它会仅如果第一个不匹配。test
的另一个重载有点有趣:它定义了一个带有一个参数的函数:一个指向返回char
的函数的指针数组,其中数组的大小(本质上)是sizeof(stream << object)
。如果流<< object
不是有效的表达式,sizeof
将产生 0,这意味着我们创建了一个大小为零的数组,这是不允许的。这就是 SFINAE 本身发挥作用的地方。尝试用不支持operator<<
的类型替换U
将会失败,因为它会生成大小为零的数组。但是,这不是一个错误 - 它只是意味着该函数已从重载集中消除。因此,在这种情况下,只能使用另一个函数。然后,它会在下面的
enum
表达式中使用——它查看所选的test
重载的返回值,并检查它是否等于 1(如果是,则表示选择了返回char
的函数,否则,选择了返回long
的函数)。结果是,如果
some_ostream <<
会编译,如果不能编译则has_inserter::value
将为1
。 object;0
。然后,我们可以使用该值来控制模板专门化,以选择正确的方式来写出特定类型的值。Warning: this is a really long explanation, but hopefully it really explains not only what SFINAE does, but gives some idea of when and why you might use it.
Okay, to explain this we probably need to back up and explain templates a bit. As we all know, Python uses what's commonly referred to as duck typing -- for example, when you invoke a function, you can pass an object X to that function as long as X provides all the operations used by the function.
In C++, a normal (non-template) function requires that you specify the type of a parameter. If you defined a function like:
You can only apply that function to an
int
. The fact that it usesx
in a way that could just as well apply to other types likelong
orfloat
makes no difference -- it only applies to anint
anyway.To get something closer to Python's duck typing, you can create a template instead:
Now our
plus1
is a lot more like it would be in Python -- in particular, we can invoke it equally well to an objectx
of any type for whichx + 1
is defined.Now, consider, for example, that we want to write some objects out to a stream. Unfortunately, some of those objects get written to a stream using
stream << object
, but others useobject.write(stream);
instead. We want to be able to handle either one without the user having to specify which. Now, template specialization allows us to write the specialized template, so if it was one type that used theobject.write(stream)
syntax, we could do something like:That's fine for one type, and if we wanted to badly enough we could add more specializations for all the types that don't support
stream << object
-- but as soon as (for example) the user adds a new type that doesn't supportstream << object
, things break again.What we want is a way to use the first specialization for any object that supports
stream << object;
, but the second for anything else (though we might sometime want to add a third for objects that usex.print(stream);
instead).We can use SFINAE to make that determination. To do that, we typically rely on a couple of other oddball details of C++. One is to use the
sizeof
operator.sizeof
determines the size of a type or an expression, but it does so entirely at compile time by looking at the types involved, without evaluating the expression itself. For example, if I have something like:I can use
sizeof(func())
. In this case,func()
returns anint
, sosizeof(func())
is equivalent tosizeof(int)
.The second interesting item that's frequently used is the fact that the size of an array must be positive, not zero.
Now, putting those together, we can do something like this:
Here we have two overloads of
test
. The second of these takes a variable argument list (the...
) which means it can match any type -- but it's also the last choice the compiler will make in selecting an overload, so it'll only match if the first one does not. The other overload oftest
is a bit more interesting: it defines a function that takes one parameter: an array of pointers to functions that returnchar
, where the size of the array is (in essence)sizeof(stream << object)
. Ifstream << object
isn't a valid expression, thesizeof
will yield 0, which means we've created an array of size zero, which isn't allowed. This is where the SFINAE itself comes into the picture. Attempting to substitute the type that doesn't supportoperator<<
forU
would fail, because it would produce a zero-sized array. But, that's not an error -- it just means that function is eliminated from the overload set. Therefore, the other function is the only one that can be used in such a case.That then gets used in the
enum
expression below -- it looks at the return value from the selected overload oftest
and checks whether it's equal to 1 (if it is, it means the function returningchar
was selected, but otherwise, the function returninglong
was selected).The result is that
has_inserter<type>::value
will be1
ifsome_ostream << object;
would compile, and0
if it wouldn't. We can then use that value to control template specialization to pick the right way to write out the value for a particular type.如果您有一些重载的模板函数,则在执行模板替换时,某些可能使用的候选函数可能无法编译,因为被替换的内容可能没有正确的行为。这不被视为编程错误,失败的模板只是从可用于该特定参数的集合中删除。
我不知道 Python 是否有类似的功能,也不太明白为什么非 C++ 程序员应该关心这个功能。但如果您想了解有关模板的更多信息,最好的书籍是 C++ 模板:完整指南 。
If you have some overloaded template functions, some of the possible candidates for use may fail to be compilable when template substitution is performed, because the thing being substituted may not have the correct behaviour. This is not considered to be a programming error, the failed templates are simply removed from the set available for that particular parameter.
I have no idea if Python has a similar feature, and don't really see why a non-C++ programmer should care about this feature. But if you want to learn more about templates, the best book on them is C++ Templates: The Complete Guide.
SFINAE 是 C++ 编译器在重载解析期间过滤掉某些模板化函数重载的原则 (1)
当编译器解析特定函数调用时,它会考虑一组可用的函数和函数模板声明来查找确定将使用哪一个。基本上,有两种机制可以做到这一点。一种可以被描述为句法。给定声明:
解析
f((int)1)
将删除版本 2 和 3,因为int
不等于complex
或T*
表示某些T
。同样,f(std::complex(1))
将删除第二个变体,f((int*)&x)
将删除第三个变体。编译器通过尝试从函数参数推导模板参数来实现此目的。如果推导失败(如T*
针对int
),则重载将被丢弃。我们想要这个的原因很明显 - 我们可能想要对不同类型做稍微不同的事情(例如,复数的绝对值由 x*conj(x) 计算并产生实数,不是复数,这与浮点数的计算不同)。
如果您之前做过一些声明式编程,则此机制类似于(Haskell):
C++ 进一步采取的方式是,即使推导类型正确,推导也可能失败,但回代到其他类型会产生一些“无意义”的结果(稍后会详细介绍)。例如:
在推导
f('c')
时(我们使用单个参数调用,因为第二个参数是隐式的):T
与char 进行匹配
生成的T
为char
,T
替换为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
表示T = int
或T = A
,其中A
是没有嵌套类型的类称为类型
。int C::*
forC = int
这种机制与我所知道的其他编程语言中的任何机制都不相似。如果你要在 Haskell 中做类似的事情,你会使用更强大的防护,但在 C++ 中是不可能的。
1:或在谈论类模板时部分模板专业化
SFINAE is a principle a C++ compiler uses to filter out some templated function overloads during overload resolution (1)
When the compiler resolves a particular function call, it considers a set of available function and function template declarations to find out which one will be used. Basically, there are two mechanisms to do it. One can be described as syntactic. Given declarations:
resolving
f((int)1)
will remove versions 2 and three, becauseint
is not equal tocomplex<T>
orT*
for someT
. Similarly,f(std::complex<float>(1))
would remove the second variant andf((int*)&x)
would remove the third. The compiler does this by trying to deduce the template parameters from the function arguments. If deduction fails (as inT*
againstint
), the overload is discarded.The reason we want this is obvious - we may want to do slightly different things for different types (eg. an absolute value of a complex is computed by
x*conj(x)
and yields a real number, not a complex number, which is different from the computation for floats).If you have done some declarative programming before, this mechanism is similar to (Haskell):
The way C++ takes this further is that the deduction may fail even when the deduced types are OK, but back substitution into the other yield some "nonsensical" result (more on that later). For example:
when deducing
f('c')
(we call with a single argument, because the second argument is implicit):T
againstchar
which yields triviallyT
aschar
T
s in the declaration aschar
s. This yieldsvoid f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
.int [sizeof(char)-sizeof(int)]
. The size of this array may be eg. -3 (depending on your platform).<= 0
are invalid, so the compiler discards the overload. Substitution Failure Is Not An Error, the compiler won't reject the program.In the end, if more than one function overload remains, the compiler uses conversion sequences comparison and partial ordering of templates to select one that is the "best".
There are more such "nonsensical" results that work like this, they are enumerated in a list in the standard (C++03). In C++0x, the realm of SFINAE is extended to almost any type error.
I won't write an extensive list of SFINAE errors, but some of the most popular are:
typename T::type
forT = int
orT = A
whereA
is a class without a nested type calledtype
.int C::*
forC = int
This mechanism is not similar to anything in other programming languages I know of. If you were to do a similar thing in Haskell, you'd use guards which are more powerful, but impossible in C++.
1: or partial template specializations when talking about class templates
Python根本不会帮助你。但您确实说您已经基本熟悉模板了。
最基本的 SFINAE 构造是使用
enable_if
。唯一棘手的部分是class enable_if
并没有封装 SFINAE,它只是公开它。在 SFINAE 中,有一些结构设置错误条件(此处为 class enable_if)和许多并行的、否则会发生冲突的定义。除了一个定义之外,所有定义都会出现一些错误,编译器会选择并使用该定义,而不会抱怨其他定义。
什么样的错误是可以接受的,这是一个最近才标准化的主要细节,但您似乎并没有问这个问题。
Python won't help you at all. But you do say you're already basically familiar with templates.
The most fundamental SFINAE construct is usage of
enable_if
. The only tricky part is thatclass enable_if
does not encapsulate SFINAE, it merely exposes it.In SFINAE, there is some structure which sets up an error condition (
class enable_if
here) and a number of parallel, otherwise conflicting definitions. Some error occurs in all but one definition, which the compiler picks and uses without complaining about the others.What kinds of errors are acceptable is a major detail which has only recently been standardized, but you don't seem to be asking about that.
Python 中没有任何东西与 SFINAE 非常相似。 Python 没有模板,当然也没有像解析模板特化时那样的基于参数的函数解析。 Python 中的函数查找纯粹是通过名称来完成的。
There is nothing in Python that remotely resembles SFINAE. Python has no templates, and certainly no parameter-based function resolution as occurs when resolving template specialisations. Function lookup is done purely by name in Python.