SFINAE
- SFINAE(Substitution Failure Is Not An Error) 是C++ 的一种语言属性,具体内容就是”从一组重载函数中删除模板实例化无效的函数”。
- SFINAE 应用最为广泛的场景是C++中的 std::enable_if,这里有完整的英文描述:
对一个函数调用进行模板推导时,编译器会尝试推导所有的候选函数(重载函数,模板,但是普通函数的优先级要高),以确保得到一个最完美的匹配。
也就是说在推导的过程中,如果出现了无效的模板参数,则会将该候选函数从重载决议集合中删除,只要最终得到了一个 perfect match ,编译就不会报错。
代码示例
long multiply(int i, int j) { return i * j; } #1
template <class T>
typename T::multiplication_result multiply(T t1, T t2) #2
{
return t1 * t2;
}
int main(void)
{
multiply(4, 5); //最佳匹配为int multiply(int,int),T为int为最佳;但是int没有multiplication_result成员,所以,使用#1,
}
main 函数调用 multiply 会使编译器会尽可能去匹配所有候选函数,虽然第一个 multiply 函数明显是较优的匹配,但是为了得到一个最精确的匹配,编译器依然会尝试去匹配剩下的候选函数,此时就会去推导 第二个multiply 函数,中间在参数推导的过程中出现了一个无效的类型 int::multiplication_result ,但是因为 SFINAE 原则并不会报错。
std::enable_if<> 的实现
前面我们在介绍 std::enable_if 的时候提到,如果 condition 不满足的话,会生成一个无效的类型,此处由于 SFINAE 机制的存在,只要 call 存在一个匹配的话,就不会报错(只是简单的丢弃该函数)。
std::enable_if<>的实现机制如下代码所示:
template<bool Cond, typename T = void> struct enable_if {};
template<typename T> struct enable_if<true, T> { typedef T type; };
从上面的代码可以看到,在 condition 为真的时候,由于偏特化机制,第二个结构体模板明显是一个更好的匹配,所以 std::enable_if<>::type 就是有效的。当 condition 为假的时候,只有第一个结构体模板能够匹配,所以 std::enable_if<>::type 是无效的,会被丢弃。
使用 enable_if 的时候,对参数有这两方面的限制。
member type definition
type T (defined only if Cond is true)
代码示例
// enable_if example: two ways of using enable_if
#include <iostream>
#include <type_traits>
// 1. the return type (bool) is only valid if T is an integral type:
template <class T>
typename std::enable_if<std::is_integral<T>::value,bool>::type //返回仅当类型为整形
is_odd (T i) {return bool(i%2);}
// 2. the second template argument is only valid if T is an integral type:
template < class T,
class = typename std::enable_if<std::is_integral<T>::value>::type>
bool is_even (T i) {return !bool(i%2);}
int main() {
short int i = 1; // code does not compile if type of i is not integral;如果 inr为非整型,代码不会编译; std::cout << std::boolalpha;
std::cout << "i is odd: " << is_odd(i) << std::endl;
std::cout << "i is even: " << is_even(i) << std::endl;
return 0;
}
模板参数列表
如同函数有其参数,模板也有其参数
模板形参参数列表
- 为了使得类型也能进行参数化
//1.覆盖的作用域,模板开始,到右括号结束
//2.定义的模板参数列表就是为了类型参数化,专门来接收一个类型
template<typename T> // <typename T>
模板实参参数列表(实例化参数列表)
compare<char *>(1,2) //<char *>
函数模板
//函数模板不进行编译,没有任何的实例化,没有产生任何的函数
//模板的实例化是在编译阶段,而每个*.cpp是单独编译的
template<typename T> //定义的模板参数列表就是为了类型参数化,专门来接收一个类型
bool compare(T a,T b)//其为函数模板,compare为模板名,而不能作为函数名
{
cout<<"template compare"<<endl;
return a>b;
}
模板的实例化
- 如果函数模板不进行编译,那么没有任何的实例化,没有产生任何的函数,模板的实例化是在编译阶段
- 实例化于调用点实例化
- 分为 隐式实例化和显示实例化
/*当进行编译时,compare<int>(1,2),将产生一下代码
编译器通过传入类型参数生成一份新的代码
函数模板--》实例化--》模板函数
*/
bool compare<int>(int a,int b) //此时compare<int>是一个模板函数
{
cout<<"template compare"<<endl;
return a>b;
}
隐式实例化
- 没有指定编译器要实例化什么样的模板函数,仅是指定模板函数的模板实参
显示实例化
- 为什么模板无法写在原文件中,而是写在头文件中?
模板的代码需要进行实例化,不进行实例化不会产生相应代码,而如果必须写在原文件中,则必须显示实例化 - 模板代码一般还是写在头文件中,#include引入产开。所以显示实例化很少利用到
template<typename T>
bool compare(T a,T b) //此时compare<int>是一个模板函数
{
cout<<"template compare"<<endl;
return a>b;
}
template bool compare<int>(int,int);//显示实例化
template bool compare<double>(double,double);//显示实例化
模板函数
- 函数模板被实例化后得到模板函数,函数模板–》实例化–》模板函数
函数模板的特化(专用化)
- 函数模板只支持模板的完全特例化,不支持部分特例化(类模板才支持)
- 编译模板时,编译器会先识别是否有特例化版本
模板的特例化(有些类型模板函数无法通用)
- 特例化是一种特殊的实例化,编译器会先识别是否有特例化版本
- 函数模板只支持模板的完全特例化,不支持部分特例化(类模板才支持)
template<typename T> //定义的模板参数列表就是为了类型参数化,专门来接收一个类型
bool compare(T a,T b)//其为函数模板,compare为模板名,而不能作为函数名
{
cout<<"template compare"<<endl;
return a>b;
}
/*
//此时推演出来的代码,不能满足char *类型的比较,即需要实例化模板
bool compare<char *>(char * a,char * b)
{
cout<<" template compare"<<endl;
return a>b;
}
*/
template<> //模板参数列表不能省略
bool compare<char *>(char * a,char * b)
{
cout<<"compare<char *>(char * a,char * b) "<<endl;
return strcmp(a,b) > 0? true:false ;
}
template<>
bool compare<const int>(const int a,const int b) //函数签名compare<const int>才算是函数名(字符串),函数参数const省略
{
cout<<"compare<const int>(const int a,const int b) "<<endl;
return a>b;
}
int main()
{
compare<char *>("19","20");
compare<int>(19,20);//其不会调用特例版本,推导的compare<int>函数名与特例版本不一致
}
template<typename T>
int FindValIndex(T* array, int size, const T& val) //const修饰变量的引用&val
{
for (int i = 0; i < size; i++)
{
if (array[i] == val)
{
return i;
}
return -1;
}
}
template<>
int FindValIndex<char*>(char** array, int size,char* const & val)
{
for (int i = 0; i < size; i++)
{
if (strcmp(array[i],val)==0)
{
return i;
}
return -1;
}
}
模板函数的重载
template<typename T> //定义的模板参数列表就是为了类型参数化,专门来接收一个类型
bool compare(T a,T b)//其为函数模板,compare为模板名,而不能作为函数名
{
cout<<"template compare"<<endl;
return a>b;
}
template<>
bool compare<int >(int a,int b)
{
cout<<" template compare"<<endl;
return a>b;
}
bool compare(int a,int b)
{
cout<<"compare<const int>(const int a,const int b) "<<endl;
return a>b;
}
函数模板的实参推演
- 模板实参推演 通过实参的类型—》(推演)–》模板类型参数是什么,即必须模板列表出现在了形参列表中
- 由于类名不像函数那样有函数形参可以推导,所以在使用类模板实例化必须指定类模板的实参列表,而不能进行类型推演
- 类型推演都需要注意二义性问题,模板可通过参加模板形参参数个数来处理
- 大部分标准库中的算法都是模板函数,而由于模板类型推演,让我们能像使用简单的函数一样使用它们
bool compare(T a,T b)
{
cout<<"template compare"<<endl;
return a>b;
}
compare(19,20); //实参推演
仅仅从模板角度来看compare(19,29)是无法编译通过的,因为compare表示的是函数模板,而不是函数,调用应该是模板函数(),即compare(19,20),而由于实参类型推演的存在,使得模板函数可简写
template<typename T>
T fun(){}
template<typename T>
T fun(int a){
T a;
return a;
}
以上代码都是不能进行实参类型推演的,因为其没有可推演的形参,都需要写明模板函数
实参推演简化的模板实参列表
- 增加模板形参个数
template<typename T,typename E>
bool compare(T a,E b)
{
cout<<"template compare"<<endl;
return a>b;
}
compare<int>(10,2.5); //明确告诉编译器第一个实参推演为int
实参推演范围
#include<iostream>
using namespace std;
template<class T>
void fun(T a)//一般模板函数,只要是类型就行
{
T x,y;
}
template<class T>//模板函数
void fun(T * a)//只接受指针类型
{
T x,y;
}
template<>
void fun<char *>(char * a)只接受char *类型
{
}
template<class T=int,class N=3>
void fun(T a)
{
T array[N];
}
//fun(3) 其产生了一个 int array[3]
C11函数模板的默认模板参数
- 在C98中,类模板就可以有默认的模板参数
- 当所有模板参数都有默认参数时,函数模板的调用如同普通函数。
- 对于类模板而言,哪怕所有参数都有默认参数,在使用时也必须在模板名后跟随<>来实例化。
- 除了上面提到的部分之外,函数模板的默认模板参数在使用规则上和其他的默认参数也有一些不同,它没有必须写在参数表最后的限制,类模板也不行。
- 使用时当存在多个模板参数时,最好显示指定模板实参列表,否则容易让人类型推演和默认模板混乱
template<typename T,typename U=int,U I=0>
struct MyStruct{};
template<typename T=int>
void fun(void){}
fun(1);
函数模板的使用
/*
* 冒泡思想,两俩比较将最大值下沉
*
*/
template<typename T>
void Swap(T& a, T& b)
{
T tmp=T();
tmp = a;
a = b;
b = tmp;
}
template<typename size_t I,typename T>
void Sort(T *array)
{
for (int i = 0; i < I-1; i++)
{
for (int j = i + 1; j < I; j++)
{
if (array[i] > array[j])
{
//Swap(array[i], array[j]);
std::swap(array[i], array[j]);
}
}
}
}
int main()
{
int array[10];
for (int i = 0; i < 10; i++)
{
array[i]=rand() % 100;
}
// Sort<int,10>(array);
Sort<10>(array);
for (auto x : array)
{
cout << x<<" ";
}
}
类模板
- 由于类名不像函数那样有函数形参可以推导,所以在使用类模板实例化必须指定类模板的实参列表,而不能进行类型推演
- 类模板除了构造和析构函数的名字可以省略参数列表,其他地方不能省略
- 类模板选择性实例化,成员方法只有在使用(调用)时才会被实例化,不使用就不会进行实例化,而编译器从上到下处理,模板代码,编译器不会进行编译,仅会检查模板函数、模板类原型是否符合模板的语法,只有在调用时,才会进行生成模板类或模板函数进行编译,所以针对模板原型
- 对于类模板而言,哪怕所有参数都有默认参数,在使用时也必须在模板名后跟随<>来实例化*
#include<iostream>
using namespace std;
//前向声明
template<typename T>
class CLink;
template<typename T> //模板参数列表
class Node //Node此时不是类名称,而是类模板名称,要将其当做类名,必须指定模板类的实参列表
{
//template<typename T>
friend class CLink<T>;针对一一对应关系
public:
//Node<T>();
//~Node<T>();
Node(T data = T()) :_data(data), _pnext(nullptr) {}
~Node() {};
private:
T _data;
Node<T>* _pnext;//Node<T> 才是类型,Node只是类模板
};
template<typename T=int>
class CLink
{
public:
CLink()
{
_phead = new Node<T>();
}
~CLink()
{
Node<T>* pcur = _phead;
while (_phead)
{
_phead = _phead->_pnext;
delete pcur;
pcur = _phead;
}
}
void show();
void InsertValHead(const T& val);
private:
Node<T>* _phead;
};
template<typename T>
void CLink<T>::InsertValHead(const T& val)
{
Node<T>* tmp = new Node<T>(val);
tmp->_pnext = _phead->_pnext;
_phead->_pnext = tmp;
}
template<typename T>
void CLink<T>::show()
{
Node<T>* pcur = _phead->_pnext;
while (pcur)
{
cout<<pcur->_data;
pcur = pcur->_pnext;
}
}
int main()
{
CLink<> clink;
for (int i = 0; i < 10; i++)
{
clink.InsertValHead(i);
}
clink.show();
}
类模板的特化(专用化)
- 对于一个类,其成员方法大多需要进行特例化处理相应类型时,可直接对类进行特例化
- 类的特例化其成员方法在类外定义不需要前置template<>
- 特例化版本不能和默认参数同时使用
成员模板
- 根据类型推演和编译器的默认类型转换达到合理的拷贝构造关系
模板其他知识
模板的非类型参数
//template<typename size_t I,typename T> //模板非类型参数本身就是一个常量
template< size_t I,typename T>
模板与const、volatile
- 我们知道const、volatile当修饰右边有*或&、[]时,其才不会被编译器忽略
- 模板的特化不能改变原来模板const修饰的类型规则
template<typename T>
bool compare(const T a,const T b)//const修饰离其最近的类型,即在模板中const直接作用于变量
{
cout<<"template compare"<<endl;
return a>b;
}
template<>
bool compare<char *>(char * const a,char * const b) //right
{
cout<<"compare<char *>(char * a,char * b) "<<endl;
return strcmp(a,b) > 0? true:false ;
}
模板的偏特化
- 针对特定的类型去专门设计,泛化和特化的问题
1. 个数的偏
- 模板的特化需要按照模板参数顺序
2. 范围的偏
- 一个类型T变 T*,其范围变小
模板模板参数
- 其可将模板作为模板参数传入,用于传入一个模板,其第一模板实参作为传入模板的类型
#include<iostream>
#include<type_traits>
#include<iterator>
#include<vector>
#include<typeinfo>
//传入容器后通过萃取得到其类型,调用方式传入确定的不同的容器 TestMoveFuntion(std::vector<std::string>());
template<typename Container>
void TestMoveFuntion(Container c)
{
using Type = typename std::iterator_traits<typename Container::iterator>::value_type;
for (int i = 0; i < 10; i++)
{
c.insert(c.end(), Type());
}
std::cout << typeid(Type).name();
}
从以上代码可见,调用时传入确定的不同的容器来相应处理。另外可通过模板模板参数来进行设计,将不需要萃取类型
泛化的模板的默认模板参数
template<typename T=int,T U=0>
- T可以设置一个默认类型
- 非类型参数也可进行默认值
模板类型别名
using在C++11中引入了新的特性,使用 typedef 重定义类型是很方便的,但它也有一些限制,比如,无法重定义一个模板。—>作为一个模板只有在实例时才会生效,所以会出现以下代码
//在 C++98/03 中往往不得不这样写:都是历史遗留问题
template<typename Val>
struct str_map
{
typedef std::map<std::string,Val> type;
};
str_map<int>::type map1;//实例化
//改进
template <typename Val>
using str_map_t = std::map<std::string, Val>;
// ...
str_map_t<int> map1;
从开发者角度看模板
- 模板为了实现泛化,其只是个半成品,具体到使用还得依赖使用者,如对象重载的运算符,模板的特化都是根据用户实际情况扩展
类型萃取
迭代器类型萃取
- 根据迭代器获取容器类型等信息