更多 C++ 习语/SFINAE
从一组重载函数中删除那些无法生成有效模板实例化的函数。
Substitution Failure Is Not An Error(替换失败并非错误)
严格来说,SFINAE 是一个语言特性,而不是一个习语。然而,这个语言特性在使用 enable-if 时被以非常习惯性的方式利用。
在模板参数推导过程中,C++ 编译器会尝试实例化多个候选重载函数的签名,以确保只有一个重载函数可以完美匹配给定的函数调用。如果在函数模板实例化期间形成了无效的参数或返回值类型,则该实例化将从重载解析集中删除,而不是导致编译错误。只要只有一个函数可以调用,编译器就不会发出任何错误。
例如,考虑一个简单的函数 multiply 及其模板化对应物。
long multiply(int i, int j) { return i * j; }
template <class T>
typename T::multiplication_result multiply(T t1, T t2)
{
return t1 * t2;
}
int main(void)
{
multiply(4,5);
}
在 main
中调用函数 multiply
会导致编译器尝试实例化模板化函数的签名,即使第一个 multiply
函数是更好的匹配。在实例化期间,会产生一个无效类型:int::multiplication_result
。然而,由于 SFINAE,这种无效实例化会自动被忽略。最终,只有一个 multiply
函数可以被调用,即第一个函数。因此编译成功。
SFINAE 通常被用来在编译时确定类型的属性。例如,考虑以下 is_pointer
元函数,它在编译时确定给定类型是否为某种类型的指针。
template <class T>
struct is_pointer
{
template <class U>
static char is_ptr(U *);
template <class X, class Y>
static char is_ptr(Y X::*);
template <class U>
static char is_ptr(U (*)());
static double is_ptr(...);
static T t;
enum { value = sizeof(is_ptr(t)) == sizeof(char) };
};
struct Foo {
int bar;
};
int main(void)
{
typedef int * IntPtr;
typedef int Foo::* FooMemberPtr;
typedef int (*FuncPtr)();
printf("%d\n",is_pointer<IntPtr>::value); // prints 1
printf("%d\n",is_pointer<FooMemberPtr>::value); // prints 1
printf("%d\n",is_pointer<FuncPtr>::value); // prints 1
}
上面的 is_pointer
元函数如果没有 SFINAE 就不会起作用。它定义了 4 个重载的 is_ptr
函数,其中 3 个是模板,每个模板都接受一个参数:指向变量的指针、指向成员变量的指针或简单的函数指针。所有三个函数都返回一个 char
,这是故意的。最后一个 is_ptr
函数是一个通配函数,使用省略号作为参数。但是,这个函数返回一个 double
,它的大小始终大于字符。
当 is_pointer
传递一个实际上是指针的类型(例如,IntPtr)时,由于两个 sizeof
表达式的比较,value
会被初始化为 true。第一个 sizeof
表达式调用 is_ptr
。如果它是一个指针,只有一个重载的模板函数匹配,而不是其他函数。但是,由于 SFINAE,不会引发错误,因为至少找到了一个合适的函数。如果没有任何函数适合,则会使用带有省略号的函数。但是,该函数返回一个 double
,它大于字符,因此 sizeof
比较失败,value
被初始化为 false。
请注意,所有 is_ptr
函数都没有定义。只有声明足以触发编译器中的 SFINAE 规则。但是,这些函数本身必须是模板。也就是说,具有常规函数的类模板将不会参与 SFINAE。参与 SFINAE 的函数必须是模板。