提问人:Jim 提问时间:8/5/2010 最后编辑:sbiJim 更新时间:11/3/2023 访问量:8147
向非 C++ 程序员解释 C++ SFINAE
Explain C++ SFINAE to a non-C++ programmer
问:
C++ 中的 SFINAE 是什么?
你能用不精通 C++ 的程序员可以理解的语言来解释它吗?另外,SFINAE 对应于 Python 等语言中的什么概念?
答:
如果有一些重载的模板函数,则在执行模板替换时,某些可能使用的候选函数可能无法编译,因为被替换的事物可能没有正确的行为。这不被视为编程错误,失败的模板只是从该特定参数的可用集合中删除。
我不知道 Python 是否有类似的功能,也不明白为什么非 C++ 程序员应该关心这个功能。但是,如果您想了解有关模板的更多信息,那么关于模板的最佳书籍是 C++ 模板:完整指南。
评论
Python 中没有任何内容与 SFINAE 非常相似。Python 没有模板,当然也没有像解析模板专用化时那样基于参数的函数解析。在 Python 中,函数查找完全是按名称完成的。
评论
Python 根本帮不了你。但你确实说你已经基本熟悉模板了。
最基本的 SFINAE 结构是 的用法。唯一棘手的部分是它没有封装 SFINAE,它只是暴露了它。enable_if
class enable_if
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
什么样的错误是可以接受的,这是一个主要的细节,直到最近才被标准化,但你似乎没有问这个问题。
评论
class "enable_if<false>" has no member "type"
cannot overload functions distinguished by return type alone
function
警告:这是一个非常长的解释,但希望它不仅能真正解释 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; }
现在,我们更像是在 Python 中——特别是,我们可以同样很好地调用它来定义任何类型的对象。plus1
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;
};
这里我们有两个重载 .其中第二个采用变量参数列表(),这意味着它可以匹配任何类型——但它也是编译器在选择重载时做出的最后选择,因此只有在第一个不匹配时才会匹配。的另一个重载更有趣:它定义了一个接受一个参数的函数:一个指向返回 的函数的指针数组,其中数组的大小是(本质上)。如果表达式无效,则将产生 0,这意味着我们创建了一个大小为零的数组,这是不允许的。这就是 SFINAE 本身的用武之地。尝试替换不支持的类型将失败,因为它将生成一个大小为零的数组。但是,这不是一个错误——它只是意味着该函数已从重载集中消除。因此,在这种情况下,另一个函数是唯一可以使用的函数。test
...
test
char
sizeof(stream << object)
stream << object
sizeof
operator<<
U
然后在下面的表达式中使用它 - 它查看所选重载的返回值,并检查它是否等于 1(如果是,则表示选择了返回的函数,但除此之外,选择了返回的函数)。enum
test
char
long
结果是,如果编译,如果编译,则编译。然后,我们可以使用该值来控制模板专用化,以选择正确的方式写出特定类型的值。has_inserter<type>::value
1
some_ostream << object;
0
评论
char
char
stream << object
sizeof
sizeof
sizeof
size_t
size_t
decltype
test
test(decltype(ref<std::ostream>() << val<U>())*);
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
解析将删除版本 2 和版本 3,因为不等于 或 对于某些 .同样,将删除第二个变体并删除第三个变体。编译器通过尝试从函数参数中推断模板参数来实现此目的。如果扣除失败(如 against ),则丢弃重载。f((int)1)
int
complex<T>
T*
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
- 编译器将声明中的所有 S 替换为 S。这会产生 .
T
char
void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
- 第二个参数的类型是指向数组的指针。该数组的大小可能是 eg。-3(取决于您的平台)。
int [sizeof(char)-sizeof(int)]
- 长度数组无效,因此编译器会丢弃重载。替换失败不是错误,编译器不会拒绝该程序。
<= 0
最后,如果存在多个函数重载,编译器会使用转换序列比较和模板的部分排序来选择一个“最佳”的模板。
还有更多这样的“无意义”结果像这样工作,它们在标准(C++03)的列表中列举。在 C++0x 中,SFINAE 的领域扩展到几乎任何类型错误。
我不会写一个广泛的SFINAE错误列表,但一些最常见的是:
- 选择没有嵌套类型的嵌套类型。例如。 for 或 where 是一个没有嵌套类型的类,称为 。
typename T::type
T = int
T = A
A
type
- 创建非正大小的数组类型。有关示例,请参阅此 litb 的答案
- 创建指向非类类型的成员指针。 为
int C::*
C = int
这种机制与我所知道的其他编程语言中的任何机制都不相似。如果你在Haskell中做类似的事情,你会使用更强大的守卫,但在C++中是不可能的。
1:在谈论类模板时,或部分模板专业化
评论
sizeof(char)-sizeof(int)
是 类型的表达式,所以它永远不能为负数;例如,它可能是 4294967293U。size_t
下一个:显式关键字是什么意思?
评论