一、 SFINAE
在介绍 s t d : : e n a b l e _ i f std::enable\_if std::enable_if之前,先介绍一个概念: S F I N A E SFINAE SFINAE,全称是: S u b s t i t u t i o n F a i l u r e i s n o t a n E r r o r Substitution\ Failure \ is \ not\ an\ Error Substitution Failure is not an Error,即“替换失败不是一个错误”。
S F I N A E SFINAE SFINAE可以看做是 C + + C++ C++语言的一种特性或模板设计中要遵循的一个重要原则,非常重要,务必要理解好。
现在,我们通过一个小样例来了解一下 S F I N A E SFINAE SFINAE,参考下方代码:
template<typename T>
typename T::size_type mydouble(const T& t) {
return t[0] * 2;
}
比如,这里我们写了一个函数模板,返回类型是 T : : s i z e _ t y p e T::size\_type T::size_type,这就要求这个返回类型必须是个类类型,而且其中必须有个类名叫作 s i z e _ t y p e size\_type size_type。
如果此时我们调用
mydouble(15);
编译器将报错:
显然,这里编译器并不认为
m
y
d
o
u
b
l
e
mydouble
mydouble这个函数模板有问题,而是找不到匹配的模板实例。因此,这就叫作
S
F
I
N
A
E
SFINAE
SFINAE,即“替换失败不是一个错误”。
如果我们使用正确的类类型呢:
std::vector<int>vec;
vec.push_back(1);
std::cout << mydouble(vec) << "\n";
此时将编译通过,因为在 s t d : : v e c t o r std::vector std::vector中存在一个类型名 s i z e _ t y p e size\_type size_type,因此 T : : s i z e _ t y p e T::size\_type T::size_type合法。
二、 s t d : : e n a b l e _ i f std::enable\_if std::enable_if的认识与使用
2.1 s t d : : e n a b l e _ i f std::enable\_if std::enable_if的基本认识
我们先看看,在标准库中是怎么实现的 s t d : : e n a b l e _ i f std::enable\_if std::enable_if:
//struct template enable_if
template<bool _Test, class _Ty = void> //泛化版本
struct enable_if {
};
template<class _Ty> //偏特化版本
struct enable_if<true, _Ty> {
using type = _Ty;
};
首先,
s
t
d
:
:
e
n
a
b
l
e
_
i
f
std::enable\_if
std::enable_if是一个结构体模板,有一个特化版本。
当传入的第一个
b
o
o
l
bool
bool类型的参数为
t
r
u
e
true
true时,将调用特化版本,在特化版本的结构体中存在一个类型为
t
y
p
e
type
type。
这就是基于 S F I N A E SFINAE SFINAE特性实现的一个类似于编译期间条件分支的作用。
2.2 s t d : : e n a b l e _ i f std::enable\_if std::enable_if的使用
2.2.1 基本使用
下面是一个最简单的使用,参考下方代码:
void Test2() {
//条件为真,存在type成员
std::enable_if<(3 > 2)>::type* mypoint1 = nullptr; //调用了偏特化版本
//条件为假,不存在type类型成员
std::enable_if<(3 < 2)>::type* mypoint2 = nullptr; //调用了偏特化版本
}
对于上面的第一个情况,由于 ( 3 > 2 ) = t r u e (3>2) = true (3>2)=true,因此将调用偏特化版本,而偏特化版本存在 t y p e type type类型名,因此能够顺利编译通过。
而第二种情况,由于 ( 3 < 2 ) = f a l s e (3<2)=false (3<2)=false,因此将调用泛化版本,而泛化版本中不存在这么一个 t y p e type type类型名,因此无法通过编译。
2.2.2 用于函数模板
由于 s t d : : e n a b l e _ i f < > : : t y p e std::enable\_if<>::type std::enable_if<>::type是一个类型,因此它可以作为返回值,通过条件来判断是否存在这么一个返回值类型。 参考下方代码:
//std::enable_if用于函数模板中
template<typename T>
typename std::enable_if < (sizeof(T) > 2) > ::type funceb() {
std::cout << "类型大小大于2个字节,调用函数模板!\n";
}
void Test3() {
funceb<int>(); //成功
funceb<char>(); //失败,char为一个字节
}
这里 s i z e o f ( ) sizeof() sizeof()可以在编译期间确定,因此我们在调用模板的时候就能知道类型的大小了,也就可以知道是否可以调用到泛化版本还是偏特化版本了。
显然, c h a r char char类型大小为 1 1 1字节,所以无法实例化出存在 t y p e type type的模板,因此就会编译失败,而 i n t int int类型是 4 4 4字节,因此是可以的。
当 s t d : : e n a b l e _ i f std::enable\_if std::enable_if用于函数模板的时候,也可以存在形参。参考下方代码:
//也可以有参数
template<typename T>
std::enable_if_t<(sizeof(T) == 4), T> funceb3(T&& t) {
t++;
return t;
}
void Test5() {
int t = funceb3(10);
std::cout << t << "\n";
}
注意写法,这个 T T T是放在第一个 b o o l bool bool类型之后的,就像前面代码所示,默认为 v o i d void void:
2.2.3 类型简写
s t d : : e n a b l e _ i f std::enable\_if std::enable_if可以通过类型别名来简化代码,而标准库为我们实现了这个方法,我们可以通过 s t d : : e n a b l e _ i f _ t std::enable\_if\_t std::enable_if_t来作为其的别名。 参考下方代码:
//泛化版本
template<bool _Test, class _Ty = void> //泛化版本
struct enable_if {
};
//偏特化版本
template<bool _Test, class _Ty = void>
using enable_if_t = typename enable_if<_Test, _Ty>::type;
由于 t y p e n a m e e n a b l e _ i f < _ T e s t , _ T y > : : t y p e typename enable\_if<\_Test,\_Ty>::type typenameenable_if<_Test,_Ty>::type是一个类型,所以使用 u s i n g using using来取别名是可以的。
因此,我们可以这样简写:
//std::enable_if的简写
template<typename T>
std::enable_if_t<(sizeof(T) > 2)> funceb2() {
}
2.2.4 用于类模板中
在之前,我们遇到了一个问题:在拷贝构造的时候,在类中拷贝构造函数没有被调用,而是转而调用构造函数模板,那么这个问题在这里就可以使用
s
t
d
:
:
e
n
a
b
l
e
_
i
f
std::enable\_if
std::enable_if来解决,回顾问题:完美转发
这里,参考下方代码:
/使用std::enable_if解决拷贝构造函数无法被调用
class Human {
//使用别名来简化代码
template<typename T>
using StrProType = std::enable_if_t<std::is_convertible<T, std::string>::value>;
private:
std::string m_name;
public:
//完美转发构造函数模板
template<typename T, typename U = StrProType<T>>
//如果能隐式转换为string类型就可以构造
Human(T&& tmpname) :m_name(std::forward<T>(tmpname)) {
std::cout << "Human(T&& tmpname)执行了\n";
}
//拷贝构造函数
Human(const Human& th) :m_name(th.m_name) {
std::cout << "Human(Human const&th)执行了\n";
}
Human(Human&& th) :m_name(std::move(th.m_name)) {
std::cout << "Human(Human && th)执行了\n";
}
};
我们引入的新的一个模板 s t d : : i s _ c o n v e r t i b l e std::is\_convertible std::is_convertible,这是一个用于判断两个类型是否能隐式转换的函数模板,返回一个 b o o l bool bool类型。下面介绍一下简单的使用方法:
void Test6() {
//判断是否能隐式转换
std::cout << "std::string -> double: " << std::is_convertible<std::string, double>::value << "\n";
std::cout << "char -> int: " << std::is_convertible<char, int>::value << "\n";
std::cout << "const char* -> std::string: " << std::is_convertible<const char*, std::string>::value << "\n";
}
运行结果:
下面我们来看我们对构造函数模板的改造:
using StrProType = std::enable_if_t<std::is_convertible<T, std::string>::value>;
//完美转发构造函数模板
template<typename T, typename U = StrProType<T>>
//如果能隐式转换为string类型就可以构造
Human(T&& tmpname) :m_name(std::forward<T>(tmpname)) {
std::cout << "Human(T&& tmpname)执行了\n";
}
为了防止代码过长,我们使用了类型别名来简化代码。
我们利用了 s t d : : e n a b l e _ i f _ t std::enable\_if\_t std::enable_if_t,然后传入 s t d : : i s _ c o n v e r t i b l e std::is\_convertible std::is_convertible,传入当前的类型和 s t d : : s t r i n g std::string std::string类型,如果当前类型能与 s t d : : s t r i n g std::string std::string类型隐式转换,那么就会返回 t r u e true true,因此 s t d : : e n a b l e _ i f _ t std::enable\_if\_t std::enable_if_t就能被实例化出,因此就会调用构造函数模板。反之,将调用拷贝构造函数模板。
再一次调用代码:
void Test7() {
Human h1("ZhangSan");
Human h2(h1);
const char* str = "LiSi";
Human h3(str);
}
这一次,成功调用了拷贝构造函数: