目录
1.什么是SFINAE
SFINAE 技术,即匹配失败不是错误,英文Substitution Failure Is Not An Error,其作用是当我们在进行模板特化的时候,会去选择那个正确的模板,避免失败。
SFINAE一般用于函数重载和编译期间类型检查,标准库中很多type traits模板就是通SFINAE来实现的。
看个具体的例子:
cpp
#include <iostream>
#include <type_traits>
using namespace std;
template<typename T>
struct check_has_member_id
{
// 仅当T是一个类类型时,"U::*"才是存在的,从而这个泛型函数的实例化才是可行的
// 否则,就将触发SFINAE
template<typename U>
static void check(decltype(&U::id)){}
// // 仅当触发SFINAE时,编译器才会"被迫"选择这个版本
template<typename U>
static int check(...){}
enum {value = std::is_void<decltype(check<T>(NULL))>::value};
};
struct TEST_STRUCT
{
int rid;
};
struct TEST_STRUCT2
{
int id;
};
int main()
{
check_has_member_id<TEST_STRUCT> t1;
cout << t1.value << endl;
check_has_member_id<TEST_STRUCT2> t2;
cout << t2.value << endl;
check_has_member_id<int> t3;
cout << t3.value << endl;
return 0;
}
// g++ --std=c++11 xxx.c
核心的代码是在实例化check_has_member_id对象的时候,通过模板参数T的类型,决定了结构体中对象value的值。而value的值是通过check<T>函数的返回值是否是void决定的。如果T中含有id成员的话,那么就会匹配第一个实例,返回void;如果不包含id的话,会匹配默认的实例,返回int。
利用这个机制还可以做很多类似的判断,比如判断一个类是否是结构体。
cpp
#include <iostream>
#include <type_traits>
// 2. 判断变量是否是一个struct 或者 类
// https://www.jianshu.com/p/d09373b83f86
template <typename T>
struct check
{
template <typename U>
static void check_class(int U::*) {}
template <typename U>
static int check_class(...) {}
enum { value = std::is_void<decltype(check_class<T>(0))>::value };
};
class myclass {};
int main()
{
check<myclass> t;
std::cout << t.value << std::endl;
check<int> t2;
std::cout << t2.value << std::endl;
return 0;
}
std::is_void的用法可参考:
2.SFINAE(替换失败不是错误)
在一个函数调用的备选方案中包含函数模板时,编译器首先要决定应该将什么样的模板参数 用于各种模板方案,然后用这些参数替换函数模板的参数列表以及返回类型,最后评估替换 后的函数模板和这个调用的匹配情况(就像常规函数一样)。
但是这一替换过程可能会遇到问题:替换产生的结果可能没有意义。不过这一类型的替换不会导致错误,C++语言规则要 求忽略掉这一类型的替换结果。
考虑如下的例子:
cpp
// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N])
{
return N;
}
// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t)
{
return t.size();
}
当传递的参数是裸数组或者字符串常量时,只有那个为裸数组定义的函数模板能够匹配:
cpp
int a[10];
std::cout << len(a); // OK: only len() for array matches
std::cout << len("tmp"); //OK: only len()
如果只是从函数签名来看的话,对第二个函数模板也可以分别用 int[10]和 char const [4]替换 类型参数 T,但是这种替换在处理返回类型 T::size_type 时会导致错误。因此对于这两个调用, 第二个函数模板会被忽略掉。
如果传递的是裸指针,以上两个模板都不会被匹配上(但是不会因此而报错)。此时编译 期会抱怨说没有发现合适的 len()函数:
cpp
int* p;
std::cout << len(p); // ERROR: no matching len() function found
但是这和传递一个有 size_type 成员但是没有 size()成员函数的情况不一样。比如如果传递的参数是 std::allocator<>:
cpp
std::allocator<int> x;
std::cout << len(x); // ERROR: len() function found, but can't size()
此时编译器会匹配到第二个函数模板。因此不会报错说没有发现合适的 len()函数,而是会 报一个编译期错误说对 std::allocator而言 size()是一个无效调用。此时第二个模板函数不 会被忽略掉。
如果忽略掉那些在替换之后返回值类型为无效的备选项,那么编译器会选择另外一个参数类 型匹配相差的备选项。比如:
cpp
// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N])
{
return N;
}
// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t)
{
return t.size();
}
// 对所有类型的应急选项:
std::size_t len (...)
{
return 0;
}
此处额外提供了一个通用函数 len(),它总会匹配所有的调用,但是其匹配情况也总是所有 重载选项中最差的(通过省略号...匹配)。
对于指针,只有应急选项能够匹配上,此时编译器不会再报缺少适用 于本次调用的 len()。不过对于 std::allocator的调用,虽然第二个和第三个函数都能匹配 上,但是第二个函数依然是最佳匹配项。因此编译器依然会报错说缺少 size()成员函数。
3.通过std::decltype来SFINAE掉表达式
对于有些限制条件,并不总是很容易地就能找到并设计出合适的表达式来 SFINAE 掉函数模 板。
比如,对于有 size_type 成员但是没有 size()成员函数的参数类型,我们想要保证会忽略掉函 数模板 len()。如果没有在函数声明中以某种方式要求 size()成员函数必须存在,这个函数模 板就会被选择并在实例化过程中导致错误:
cpp
template<typename T>
typename T::size_type len (T const& t)
{
return t.size();
}
std::allocator<int> x;
std::cout << len(x) << '\n'; //ERROR: len() selected,
处理这种情况有一个常见的模式或者习惯用法:
1)通过尾置返回类型语法(函数名前用auto修饰,并在函数名后跟->,再加末尾的返回类型) 来制定返回类型。
2)使用std::decltype和逗号运算符来定义返回类型。
3)将所有必须成立的表达式放置于逗号运算符开头(表达式转换为void类型,以防逗号运算符重载)。
4)在逗号运算符末尾定义一个实际返回类型(类型为返回类型)的对象。
例如:
cpp
template<typename T>
auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() )
{
return t.size();
}
这里返回类型定义为:
cpp
decltype( (void)(t.size()), T::size_type() )
由于decltype构造的操作数是以逗号分隔的表达式列表,因此,最后一个表达式T::size_type()生成所需返回类型的值(decltype将其转换为返回类型)。(最后一个)逗号之前的表达式是必须成立的。在本例中就是t.size()。将表达式强制转换为void,是为了避免由于用户自定义重载表达式对于类型的逗号运算符而带来的问题。
请注意,decltype的实参是一个未求值的操作数。这意味着可以在不调用构造函数的情况下创建"虚对象",请参考: