在C ++中将函数重载与模板混合时,必须考虑一个有趣的问题。模板的问题在于它们通常包含过多的内容,并且与重载混合使用时,结果可能令人惊讶:
#include <iostream>
void foo(unsigned i) {
std::cout << "This is unsigned Function : " << i << "\n";
}
template <typename T>
void foo(const T& t) {
std::cout << "This is template Function : " << t << "\n";
}
int main() {
foo(-66);
foo(77);
foo(77u);
return 0;
}
默认情况下整数是带符号的(它们仅在带有U后缀的情况下变为无符号)。当编译器检查用于此调用的重载候选者时,会发现第一个函数需要转换,而第二个函数则完全匹配。
当编译器查看作为模板的候选重载时,它实际上必须执行将显式指定或推导的类型替换为模板参数。并不总是生成有意义的代码,如以下示例所示;虽然是人工的,但它代表了许多用现代C ++编写的通用代码:
int negate(int i){
std::cout << "This is ordinary Function : " ;
return -i;
}
template < typename T >
typename T::value_type negate(const T& t) {
std::cout << "This is template Function : " ;
return -T(t);
}
std::cout << negate(88) << std::endl;
考虑调用negate(88)。它将接收第一个重载 并返回 -88。但是,在寻找最佳重载时,必须考虑所有候选者。当编译器考虑模板化的negate时,它将推导的推论参数类型(在这种情况下为int)代入模板中,并提出以下声明:
int::value_type negate(const int& t);
当然,此代码无效,因为int没有名为value_type的成员 。所以有人会问-在这种情况下,编译器是否应该失败并发出错误消息?当然不。如果确实如此,用C ++编写通用代码将非常困难。实际上,C ++标准在此类情况下有一个特殊的子句,准确解释了编译器应如何运行。
SFINAE
在C ++ 11标准的最新草案中,相关章节为14.8.2;它指出当发生替换失败(例如上面显示的替换失败)时,该特定类型的类型推导失败。没有任何错误。编译器只是忽略了该候选对象,而是查看其他候选对象。
在C++ folklore,此规则称为“替换失败不是错误” 或 SFINAE。
标准规定:
如果替换导致无效的类型或表达式,则类型推导将失败。 无效的类型或表达式是使用替换参数编写的格式或表达式。 只有函数类型及其模板参数类型的“直接上下文”中的无效类型和表达式才可能导致推论失败。
然后继续列出可能被视为无效的可能方案,例如使用非类的类型或限定名称中的枚举类型,尝试创建对void的引用,等等。
但是,等等,关于“直接上下文”的最后一句话是什么意思?考虑以下(非理性的)示例:
template <typename T>
void negate(const T& t) {
typename T::value_type n = -t();
}
如果类型推导与某些基本类型的重载匹配,则由于函数体内的T :: value_type实际上会产生编译错误。这超出了标准提到的“函数类型及其模板参数类型的直接上下文”。
此处的教训是,如果我们要编写仅对某些类型有意义的模板,则必须使它在声明中就对无效类型的推论失败,从而导致替换失败。如果无效类型偷偷经过重载候选选择阶段,则程序将无法编译。
enable_if-模板的编译时开关
enable_if 的定义类似于下面的代码:(只有 Cond = true 时定义了 type)
template<bool Cond, class T = void> struct enable_if {};
template<class T> struct enable_if<true, T> { typedef T type; };
这样的话,enable_if<true, T>::type
即为 T
,而 enable_if<false, T>::type
会引发编译错误(在 SFINAE 下,即不将包含这一 enable_if 的函数 / 类作为候选)。
这个模板类的实现相当的简单,看一下一个版本的实现。
template<bool B, class T = void>
struct enable_if {};
template<class T>
struct enable_if<true, T> { typedef T type; };
一个普通版本的模板类定义,一个偏特化版本的模板类定义。它在第一个模板参数为false的时候并不会定义type,只有在第一模板参数为true的时候才会定义type。看一下下面的模板实例化代码
typename std::enable_if<true, int>::type t; //正确
typename std::enable_if<true>::type; //可以通过编译,没有实际用处,推导的模板是偏特化版本,第一模板参数是true,
//第二模板参数是通常版本中定义的默认类型即void
typename std::enable_if<false>::type; //无法通过编译,type类型没有定义
typename std::enable_if<false, int>::type t2; //同上
我们可以看到,通过typename std::enable_if<bool>::type这样传入一个bool值,就能推导出这个type是不是未定义的。那么这种用法有什么用途呢?
template < class T , typename std::enable_if < std::is_integral < T > ::value ,T > ::type * = nullptr >
void do_stuff(T& t) {
std::cout << "do_stuff integral\n";
// an implementation for integral types (int, char, unsigned, etc.) 为整数类型
}
template < class T, typename std::enable_if<std::is_class<T>::value,T>::type* = nullptr >
void do_stuff(T& t) {
// an implementation for class types 为类类型
}
//这两个函数如果是普通函数的话,根据重载的规则是不会通过编译的。即便是模板函数,如果这两个函数都能推导出正确的结果,也会产生重载二义性问题。
//但是正因为std::enable_if的运用使这两个函数的返回值在同一个函数调用的推导过程中只有一个合法,遵循SFINAE原则,则可以顺利通过编译。
template <typename T>
typename std::enable_if<std::is_trivial<T>::value>::type SFINAE_test(T value)
{
std::cout<<"T is trival"<<std::endl;
}
template <typename T>
typename std::enable_if<!std::is_trivial<T>::value>::type SFINAE_test(T value)
{
std::cout<<"T is none trival"<<std::endl;
}
分析:
- do_stuff函数
注意在这里工作的SFINAE。当我们调用do_stuff(<int var>)时,编译器选择第一个重载:由于条件 std :: is_integral <int>为true,所以使用struct enable_if表示true的特殊化,其内部类型设置为INT。省略第二个重载,因为如果没有真正的专业化(std :: is_class <int>为false), 则选择struct enable_if的常规形式,并且它没有type,因此参数的类型导致替换失败。
-
std::is_trivial<T>
std::is_integral<T>::value
是一个布尔值,在T
为整型时为真,否则为假。
#include <iostream>
#include <type_traits>
using namespace std;
template<typename T, typename = typename enable_if<is_integral<T>::value, void>::type>
bool isodd(T x)
{
return x % 2;
}
int main()
{
cout << isodd(4) << endl << isodd('a');
//cout << isodd("qwq"); -- compile error
return 0;
}
- enable_if函数
当第一个函数调用进行模板函数推导的时候,第一个版本的模板函数std::is_trivial<T>::value为false,继而std::enable_if<std::is_trivial<T>::value>::type这个类型未定义,不能正确推导,编译器区寻找下一个可能的实现,所以接下来找到第二个模板函数,!std::is_trivial<T>::value的值是true,继而std::enable_if<std::is_trivial<T>::value>::type是void类型,推导成功。这时候SFINAE_test(std::string("123"));调用有了唯一确定的推导,即第二个模板函数,所以程序打印T is none trival。与此相似的过程,第二个函数调用打印出T is trival。
这样写的好处是什么?这个例子中可以认为我们利用SFINAE特性实现了通过不同返回值,相同函数参数进行了函数重载,这样代码看起来更统一一些。还有一些其他应用std::enable_if的方式,比如在模板参数列表里,在函数参数列表里,都是利用SFINAE特性来实现某一些函数的选择推导。
enable_if 已经成为Boost的一部分,自C ++ 11起,它也作为标准std :: enable_if出现在标准C ++库中。但是,它的用法有些冗长,因此C ++ 14为方便起见添加了此类型别名:
template <bool B, typename T = void>
using enable_if_t = typename enable_if<B, T>::type;
这样,可以更简洁地重写上面的示例:
template <class T,
typename std::enable_if_t<std::is_integral<T>::value>* = nullptr>
void do_stuff(T& t) {
// an implementation for integral types (int, char, unsigned, etc.)
}
template <class T,
typename std::enable_if_t<std::is_class<T>::value>* = nullptr>
void do_stuff(T& t) {
// an implementation for class types
}
enable_if的使用
enable_if是一个非常有用的工具。C ++ 11标准模板库中有数百篇引用。它非常有用,因为它是使用类型特征的关键部分。
类型特征是一种将模板限制为具有某些属性的类型的方法。没有enable_if,模板是一个比较钝的“包罗万象”工具。如果我们使用模板参数定义函数,则该函数将在所有可能的类型上调用。
Type traits和enable_if 让我们创建作用于不同类型的type的函数,同时仍然保持泛型。
我喜欢的一个用法示例是std :: vector的两个参数的构造函数:
// Create the vector {8, 8, 8, 8}
std::vector<int> v1(4, 8);
// Create another vector {8, 8, 8, 8}
std::vector<int> v2(std::begin(v1), std::end(v1));
// Create the vector {1, 2, 3, 4}
int arr[] = {1, 2, 3, 4, 5, 6, 7};
std::vector<int> v3(arr, arr + 4);
这里使用的两种参数的构造函数有两种形式。 忽略分配器,可以这样声明这些构造器:
class vector {
vector(size_type n, const T val);
template <class InputIterator>
vector(InputIterator first, InputIterator last);
...
}
这两个构造函数都带有两个参数,但是第二个具有模板的catch-all属性。即使模板参数InputIterator的有一个描述性的名称,它没有语义-编译器不会介意,如果它被称为 ARG42 或 T 。
这里的问题是,即使对于v1,如果我们不做特别的事情,第二个构造函数也会被调用。这是因为 数字 4 的类型是int而不是size_t。因此,要调用第一个构造函数,编译器将必须执行类型转换。第二个构造函数将非常适合。
那么库实现者如何避免此问题并确保仅为迭代器调用第二个构造函数?至此,我们知道了答案-enable_if。
这是真正定义第二个构造函数的方式:
template <class _InputIterator>
vector(_InputIterator __first,
typename enable_if<__is_input_iterator<_InputIterator>::value &&
!__is_forward_iterator<_InputIterator>::value &&
... more conditions ...
_InputIterator>::type __last);
它使用enable_if仅对输入迭代器的类型启用此重载,而对正向迭代器则不启用。对于正向迭代器,有一个单独的重载,因为可以更有效地实现这些构造函数。
如前所述,C ++ 11标准库中有很多enable_if的用法。该字符串::追加方法有一个非常类似用途上面,因为它有几个重载两个参数和迭代器模板超载。
一个有点不同的示例是std :: signbit,应该为所有算术类型(整数或浮点数)定义它。这是cmath标头中声明的简化版本:
template <class T>
typename std::enable_if<std::is_arithmetic<T>, bool>::type
signbit(T x)
{
// implementation
}
在不使用enable_if的情况下,请考虑库实现者将拥有的选项。一种方法是为每个已知算术类型重载函数。太冗长了。另一个方法是只使用不受限制的模板。但随后,有我们实际上通过一个错误的类型进去,说的std :: string,我们就很有可能得到一个相当模糊的错误,在使用的时候。使用enable_if,我们既不必编写样板文件,也不必产生错误的错误消息。如果我们使用错误的类型调用上面定义的std :: signbit,我们将得到一个非常有用的错误,指出找不到合适的函数。
更高版本的enable_if
诚然,std :: enable_if很笨拙,甚至使enable_if_t也没什么用,尽管它有点儿冗长。您仍然必须以一种经常混淆返回类型或参数类型的方式将其混合到函数的声明中。这就是为什么一些在线资源建议制作“高级版本”的更高级版本。我个人认为这是错误的权衡。
std :: enable_if是很少使用的构造。因此,使其不再那么冗长并不能给我们带来多少好处。另一方面,使其更加神秘是有害的,因为每次我们看到它时,我们都必须考虑它是如何工作的。此处显示的实现非常简单,我会采用这种方式。最后,我将注意到C ++标准库使用 std :: enable_if的冗长的“笨拙”版本, 而没有定义更复杂的版本。我认为这是正确的决定。
[1] | 但是,如果我们对int进行了重载,则将选择此重载,因为在重载解析中,非模板优先于模板。 |
[2] | 以前我在这里有一个版本,尽管受到早期编译器的支持,但并不完全符合标准。我已经将其修改为与现代gcc和Clang一起使用的稍微复杂一些的版本。这里的棘手之处是由于do_stuff在两种情况下都具有完全相同的签名。在这种情况下,我们必须小心确保编译器仅推断单个版本。 |
[3] | 将其视为重载和模板之间的中间路线。C ++有另一个实现类似功能的工具-运行时多态性。类型特征使我们能够在编译时执行此操作,而不会产生任何运行时成本。 |
最后完整测试代码、以及cpprefrence上的例子代码
#include <iostream>
#include <vector>
void foo(unsigned i) {
std::cout << "This is unsigned Function : " << i << "\n";
}
template <typename T>
void foo(const T& t) {
std::cout << "This is template Function : " << t << "\n";
}
int negate(int i){
std::cout << "This is ordinary Function : " ;
return -i;
}
//template < typename T >
//typename T::value_type negate(const T& t) {
// std::cout << "This is template Function : " ;
// return -T(t);
//}
template <typename T>
void negate(const T& t) {
std::cout << "This is template Function2 : ";
typename T::value_type n = -t[0];
std::cout << n << std::endl;
}
template <bool, typename T = void>
struct enable_if
{};
template <typename T>
struct enable_if<true, T> {
typedef T type;
};
template < class T , typename std::enable_if < std::is_integral < T > ::value ,T > ::type * = nullptr >
void do_stuff(T& t) {
std::cout << "do_stuff of integral type\n";
// an implementation for integral types (int, char, unsigned, etc.)
}
//template <class T, typename std::enable_if_t<std::is_integral<T>::value>* = nullptr>
// void do_stuff(T& t) {
// // an implementation for integral types (int, char, unsigned, etc.)
//}
class tst {
};
template <class T, typename std::enable_if<std::is_class<T>::value,T>::type* = nullptr >
void do_stuff(T& t) {
std::cout << "do_stuff of class types\n";
// an implementation for class types
}
//template <class T,typename std::enable_if_t<std::is_class<T>::value>* = nullptr>
// void do_stuff(T& t) {
// // an implementation for class types
//}
//这两个函数如果是普通函数的话,根据重载的规则是不会通过编译的。即便是模板函数,如果这两个函数都能推导出正确的结果,也会产生重载二义性问题。
//但是正因为std::enable_if的运用使这两个函数的返回值在同一个函数调用的推导过程中只有一个合法,遵循SFINAE原则,则可以顺利通过编译。
template <typename T>
typename std::enable_if<std::is_trivial<T>::value>::type SFINAE_test(T value)
{
std::cout << "T is trival" << std::endl;
}
template <typename T>
typename std::enable_if<!std::is_trivial<T>::value>::type SFINAE_test(T value)
{
std::cout << "T is none trival" << std::endl;
}
int main() {
foo(-66);
foo(77);
foo(77u);
std::cout << negate(88) << std::endl;
std::vector<int> vec{10};
negate(vec);
int num = 999;
do_stuff(num);
tst t;
do_stuff(t);
SFINAE_test(std::string("123"));
SFINAE_test(123);
return 0;
}
#include <type_traits>
#include <iostream>
#include <string>
namespace detail { struct inplace_t{}; }
void* operator new(std::size_t, void* p, detail::inplace_t) {
return p;
}
// enabled via the return type
template<class T,class... Args>
typename std::enable_if<std::is_trivially_constructible<T,Args&&...>::value>::type
construct(T* t,Args&&... args)
{
std::cout << "constructing trivially constructible T\n";
}
// enabled via a parameter
template<class T>
void destroy(T* t,
typename std::enable_if<std::is_trivially_destructible<T>::value>::type* = 0)
{
std::cout << "destroying trivially destructible T\n";
}
// enabled via a template parameter
template<class T,
typename std::enable_if<
!std::is_trivially_destructible<T>{} &&
(std::is_class<T>{} || std::is_union<T>{}),
int>::type = 0>
void destroy(T* t)
{
std::cout << "destroying non-trivially destructible T\n";
t->~T();
}
int main()
{
std::aligned_union_t<0,int,std::string> u;
construct(reinterpret_cast<int*>(&u));
destroy(reinterpret_cast<int*>(&u));
construct(reinterpret_cast<std::string*>(&u),"Hello");
destroy(reinterpret_cast<std::string*>(&u));
}
参考文献:
https://eli.thegreenplace.net/2014/sfinae-and-enable_if/
https://www.jianshu.com/p/a961c35910d2
https://ouuan.github.io/post/c-11-enable-if-%E7%9A%84%E4%BD%BF%E7%94%A8/