使用情景: 在 C++11 之前,SFINAE 是实现类型检查的唯一手段。例如,你想编写一个函数,它只接受数字类型,而不接受字符串或自定义类,这时就需要 SFINAE
SFINAE(Substitution Failure Is Not An Error) 是 C++ 模板元编程的基石之一,字面意思是:“替换失败并非错误”。
简单来说,当编译器在模板实例化过程中,如果在替换模板参数时发生失败(例如类型不匹配、访问不存在的成员等),编译器不会抛出编译错误,而是会将该模板从候选者列表中移除,并继续寻找其他可能的匹配。
一、工作机制
下面先从函数重载理解一下 Failure is not an error(FINAE)
1 2 3 4 5 6 7 8 9 10 11 12
| struct A {}; struct B: public A {}; struct C {};
void foo(A const&) {} void foo(B const&) {}
void callFoo() { foo( A() ); foo( B() ); foo( C() ); }
|
那么 foo( A() ) 虽然匹配 foo(B const&) 会失败,但是它起码能匹配 foo(A const&),所以它是正确的; foo( B() ) 能同时匹配两个函数原型,但是B&要更好一些,因此它选择了 B。而 foo( C() ); 因为两个函数都匹配失败(Failure)了,所以它找不到相应的原型,这时才会爆出一个编译器错误(Error)。
这样就好理解了,加上 S,就是说替换失败并不是失败。下面看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| struct X { typedef int type; };
struct Y { typedef int type2; };
template <typename T> void foo(typename T::type); template <typename T> void foo(typename T::type2); template <typename T> void foo(T);
void callFoo() { foo<X>(5); foo<Y>(10); foo<int>(15); }
|
这就是 SFINAE 机制所要实现的东西。 当我们指定 foo<Y> 的时候,Substitution 就开始工作了,而且会同时工作在三个不同的 foo 签名上。如果我们仅仅因为 Y 没有 type,就在匹配 Foo0 时宣布出错,那显然是武断的,因为我们起码能保证,也希望将这个函数匹配到 Foo1 上。
当编译器看到一个函数调用时,它会按以下步骤操作:
- 候选者收集:找出所有同名的模板函数和非模板函数。
- 替换:将实际的调用参数类型代入模板参数中。
- 判定:
- 如果代入后产生非法代码(如
int::iterator 或数组下标越界),编译器判定该模板“不适用”。
- 关键点:只要该失败发生在“模板参数推导或替换”的上下文中,编译器就会默默将其剔除,而不是报错。
- 决策:如果在剔除所有无效模板后,剩下一个最佳匹配函数,则编译成功;否则报“找不到匹配的函数”。
二、enable_if
std::enable_if 是实现 SFINAE 的核心工具。
2.1 引入
首先看一段场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| struct ICounter { virtual void increase() = 0; virtual ~ICounter() {} };
struct Counter: public ICounter { void increase() override { } };
template <typename T> void inc_counter(T& counterObj) { counterObj.increase(); }
template <typename T> void inc_counter(T& intTypeCounter){ ++intTypeCounter; }
void doSomething() { Counter cntObj; uint32_t cntUI32;
inc_counter(cntObj); inc_counter(cntUI32); }
|
我们希望任何一个调用,两个 inc_counter 只有一个是正常工作的,但是这里明显是不行的——redefinition。
这里便要使用 enable_if 来帮助我们完成对不同实例做个限定:
1 2 3 4 5 6 7 8 9 10 11 12 13
| template <typename T> void inc_counter(T &counterObj, std::enable_if_t<std::is_base_of_v<ICounter, T>> * = nullptr) { counterObj.increase(); }
template <typename T> void inc_counter(T &intTypeCounter, std::enable_if_t<std::is_integral_v<T>> * = nullptr) { ++intTypeCounter; }
|
2.2 enable_if 的使用
std::enable_if 是 C++11 引入的工具,用于实现 SFINAE。格式为 std::enable_if<Condition, T>:
- 如果
Condition 为 true,它定义一个成员 type 为 T(默认为 void)。
- 如果
Condition 为 false,则没有 type 成员,则不会产生任何类型。当编译器尝试访问这个不存在的 type 时,即触发 SFINAE。
所以:std::enable_if_t<std::is_same_v<U, int>>:
- 如果
U == int → 得到类型 void
- 如果
U != int → 产生“替换失败”
std::enable_if 定义如下:
1 2 3 4 5 6 7 8
| template <bool _Test, class _Ty = void> struct enable_if {};
template <class _Ty> struct enable_if<true, _Ty> { using type = _Ty; };
|
只有 _Test 为 true 时,类成员才有定义。
std::enable_if 可以用作函数参数、返回类型或类模板或函数模板参数,以有条件地从重载中删除函数或类。
C++ 17 后可以使用别名 enable_if_t,其定义如下:
1 2
| template <bool _Test, class _Ty = void> using enable_if_t = typename enable_if<_Test, _Ty>::type;
|
例一:作为模板参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| template <typename T> struct Check2 { template <typename U = T, typename std::enable_if_t<std::is_same_v<U, int>, int> = 0> U read() { return 42; }
template <typename U = T, typename std::enable_if_t<std::is_same_v<U, double>, int> = 0> U read() { return 3.14; } };
|
例二:作为函数参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| template<typename T>
struct Check1 { template<typename U = T> U read(typename std::enable_if_t<std::is_same_v<U, int> >* = nullptr) { return 42; } template<typename U = T> U read(typename std::enable_if_t<std::is_same_v<U, double> >* = nullptr) { return 3.14; } };
|
例三:作为返回值类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| template<typename T> struct Check3 { template<typename U = T> typename std::enable_if_t<std::is_same_v<U, int>, U> read() { return 42; }
template<typename U = T> typename std::enable_if_t<std::is_same_v<U, double>, U> read() { return 3.14; } };
|
例四:类型偏特化
1 2 3 4 5 6 7 8 9
| template<typename T, typename = void> struct zoo;
template<typename T> struct zoo<T, std::enable_if_t<std::is_integral_v<T>>> { };
|
三、实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <type_traits>
template <typename T> typename std::enable_if<std::is_integral<T>::value, void>::type process(T t) { std::cout << "Integer: " << t << std::endl; }
template <typename T> typename std::enable_if<!std::is_integral<T>::value, void>::type process(T t) { std::cout << "Non-integer: " << t << std::endl; }
int main() { process(10); process(3.14); return 0; }
|
正是由于 SFINAE 机制的存在,当 T 不满足 std::enable_if 中的条件时,编译器不会报错,而是排除该模板重载,继续寻找其他合适的模板重载,从而保证代码能够正确编译和运行。