一、定义模板
1、函数模板
以关键字template开始后接<>括起来的,以逗号分隔的一个或多个模板参数列表,且该列表不能为空;
模板类型参数:可以看作类型说明符(内置类型或类类型),类型参数可以用来指定返回类型,函数的参数类型,以及在函数体内用于变量声明或类型转换;
类型参数前面必须加关键字 typename 或 class ,
非类型参数:通过特定的类型名指定非类型参数,一个非类型参数表示一个值,而非类型;
一个非类型参数可以是整形、指向对象或函数的指针或(左值)引用;
非类型参数是整形的对应的实参必须是常量表达式,是指针或引用的对应的实参必须具有静态的生存期;
在需要常量表达式的地方可以使用非类型实参,例如数组的维度;
inline和constexpr的函数模板:
如同普通函数一样,放在返回值之前,模板参数列表之后;
泛型编程的两个重要原则:
模板中的参数是const引用:保证参数能用于能拷贝和不能拷贝的类型;
函数体中的条件判断仅用<比较运算符:降低对类型的要求,只要求<运算符,不要求>运算符;
建议:模板程序应尽量减少对实参类型的要求;
模板编译:
定义模板时编译器不会生成代码,实例化模板时,编译器才会生成代码;
因此,与非模板不同,模板的头文件既包括声明也包括定义;
大多数编译错误在实例化期间报告
编译器会在三个阶段报告错误:
1、编译模板本身时;
2、使用模板时;
3、实例化模板时;
保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作;
2、类模板
一个类模板的每个实例都会形成一个独立的类;
在模板作用域中引用模板类型:将模板自己的参数当作被使用模板的实参;
类模板的成员函数:
template <typename T>
void AA<T>::ff(){}
template <typename T>
AA<T>::AA(){}
类模板成员函数的实例化:
默认情况下,一个实例化了的类模板,其成员只有在使用时才被实例化;
在类代码内简化模板类名的使用:
template <typename T>
class AA
{
public:
AA * clone(); //在类内不需要提供模板实参
};
template <typename T>
AA<T> AA<T>::clone() //在类外需要提供模板实参
{
return new AA(*this); //在类内不需要提供模板实参
}
在一个模板的作用域内,我们可以直接使用模板名,而不必指定模板实参;
类模板和友元:
类与友元各自是否是模板是相互无关的;
通用和特定的友好关系:
template<typename T> class B; //前置声明
template<typename T>
class A
{
friend class B<T>; //只有模板参数为T的B类模板实例才是A的友元,需要前置声明
//为了让所有实例都是友元,友元声明的模板参数与类本身的模板参数必须不同;
template<typename X> friend class C; //C的所有实例都是A的友元,不需要前置声明
private:
T a;
};
令模板自己的类型参数成为友元:
template<typename T>
class A
{
friend T; //T为A的友元
int a = 1;
};
模板类型别名:
using v1 = vector<int>;
template<typename T> using v2 = vector<T>;
template<typename T> using p1 = pair<T, int>; //定义模板别名时,可以固定一个或多个参数
类模板的static成员:
类模板的每个实例都有一个独有的static对象,因此static数据成员也要定义成模板:
template <typename T>
class A
{
public:
static void f();
private:
static int a;
};
template <typename T>
int A<T>::a = 1; //static数据成员也要定义成模板
template <typename T>
void A<T>::f() {}
类似其他成员函数,static成员函数只有在使用时才会实例化;
3、模板参数
模板参数与作用域:
typedef int T;
template <typename T>
class B
{
T a; //a的类型为T,不是int
string T = 0; //错误,重声明模板参数T
};
template <typename T, class T> //错误,非法重用模板参数名
模板声明
template <typename T> void f(const T& t);
template <typename X> void f(const X& t);
template <typename U>
void f(const U& t) {}
与函数参数一致,声明的模板参数的名字不必与定义一致;
建议:模板的所有声明通常放置在文件开始的位置,出现于任何使用模板的代码之前;
使用类的类型成员:
在非模板代码中,编译器掌握类的定义,因此他知道通过作用域运算符访问的名字是类型成员还是static成员;
但模板代码只有在实例化时才能区分,但编译器又必须知道改名字是否为一个类型成员,因此可以通过关键字 typename 来处理(注意不能用class);
template <typename T>
typename T::value_type top(const T & t) //typename指定T::value_type为一个类型成员
{
if (t.empty() == false)
{
return t.front();
}
return typename T::value_type();
}
默认模板实参:
template <typename T, typename F = less<T>>
int cmp(const T& t1, const T& t2, F f = F())
{
if (f(t1, t2)) return 1;
if (f(t2, t1)) return 2;
return 0;
}
如果传递第三个实参,则该参数必须是可调用对象,且返回值为bool,且该可调用对象接受的参数必须与cmp前两个参数兼容;
类模板与模板默认实参:
如果一个类模板为其模板参数都提供了默认实参,且使用这些默认实参,则模板名之后跟<>
template <class T = int>
class A
{
T t = 0;
};
A <> a;
4、成员模板
普通(非模板)类的成员模板
类模板的成员模板
类个成员各自有自己的、独立的模板参数;
在类外定义成员模板时,类模板参数列表在前,成员模板的参数列表在后;
template<typename T>
class A
{
public:
template <class X> A();
};
template<typename T> //类模板参数列表在前
template <class X> //成员模板的参数列表在后
A<T>::A() {}
5、控制实例化
在多个文件中实例化相同模板的额外开销可能很大,可以通过显示实例化避免这种额外开销;
extern template declarartion //实例化声明
template declarartion //实例化定义
对于一个给定的实例化版本,声明可以有多个但定义只能有一个;
extern template class A<int>;
A<int> a; //实例化出现在其他位置
A<string> a; //在本文件实例化
template class A<int>; //在此文件中定义实例化
对于每个实例化声明,在程序的某个位置必须有其显示的实例化定义;
与普通实例化不同,一个类模板的实例化定义会实例化该模板的所有成员,且所用类型必须能用于模板的所有成员;
显示实例化定义中,模板实参如果时类类型,则该类必须有默认的构造函数;
class A
{
public:
A(int a){}
};
template class vector<A>; //错误,A没有默认构造函数
6、效率与灵活性
在运行时绑定删除器:在运行时绑定删除器,shared_ptr使用户重载删除器更为方便
对于shared_ptr来说,删除器是可以重载的,所以其类型是在运行时绑定,通常类成员的类型在运行时是不能改变的,因此不能将删除器直接保存为一个成员;
在编译时绑定删除器:在编译时绑定删除器,unique_ptr避免了间接调用删除器的运行时开销
删除器类型是unique_ptr类型的一部分,因此删除器类型在编译时是知道的,所以删除器可以直接保存在unique_ptr对象中;
二、模板实参推断
1、类型转换与模板类型参数
类型转换应用于函数模板的包括如下两项:
可以将一个非const对象的引用或指针传递给const引用或指针形参;
如果函数形参不是引用类型,则可以对数组或函数类型的实参应用于正常的指针转换;
除此之外其他的类型转化:算术转换,派生类向基类的转换用户自定义的转换是不能应用于函数模板的;
使用相同模板类型参数的函数形参:
long lng;
cmp(lng, 1); //错误两个形参类型不一致
如果希望进行正常的类型转换,可以定义多个模板类型参数;
如果函数形参不是模板类型参数,则对实参进行正常的类型转换;
2、函数模板显示实参
某些情况下,编译器无法推断出模板实参的类型,例如返回类型与参数列表任何类型都不相同;
template <class T1,class T2,class T3>
T1 sum1(T2, T3);
T3 sum2(T1, T2);
指定显示模板实参:
显示模板实参在尖括号中给出,在函数名之后参数列表之前;
显示模板实参按从左至右的顺序,与模板参数匹配;
sum1<long, int>(1, 2); //T1为long,T2为int,T3自动推导为int
sum2<long, int>(1, 2); //错误,T3返回类型无法推断
对于指定了显示模板实参的模板参数,可以进行正常的类型转换:
template <class T>
void f(T, T);
long lng = 1;
f(lng, 1); //错误
f<long>(lng, 1); //正确,两个实参都为long
f<int>(lng, 1); //正确,两个实参都为int
3、尾置返回类型与类型转换
template<class T>
??? &f(T beg, T end)
{
return *beg;
}
可以用decltype(*beg)获取此表达式的类型,但编译器遇到函数的参数列表之前,beg是不存在的,所以可以采用尾置返回类型;
auto f(T beg, T end)->decltype(*beg)
{
return *beg;
}
进行类型转换的标准库模板类:头文件 #include<type_traits>
remove_reference<T>::type
若T为X&或X&&,则type为X类型,否则type为T类型
template <typename T>
auto f2(T beg, T end)->typename remove_reference<decltype(*beg)>::type //type为元素类型
{
return *beg;
}
上例中type为类模板的类型成员,所以需要typename来告诉编译器,type为一个类型,而不是static成员;
更多标准库类型转换模板参见606页表16.1,表中模板的工作方式都与remove_reference工作方式类似;
4、函数指针和实参推断
当用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器用指针类型推断模板实参;
template <class T>
void f3(const T & t) {}
void(*pf3)(const int &) = f3;
当参数是一个函数模板实例的地址时,对每个模板参数能唯一确定其类型或值;
void f(void(*pf3)(const int &)) {}
void f(void(*pf3)(const string &)) {}
f(f3); //错误,使用f3的哪个实例?
以可通过显示模板实参来消除歧义:
f(f3<int>); //正确
5、模板实参推断和引用
从左值引用函数参数推断类型:
函数参数为非const引用
template <typename T>
void f1(T&) {}
int i = 1;
const int ci = 2;
f1(i); //T是int
f1(ci); //T是const int
函数参数为const引用
template <typename T>
void f2(const T&) {}
f2(i); // T是int
f2(ci); //T是int
从右值引用函数参数推断类型:
template <typename T>
void f3(T&&) {}
f3(3); //T是int
引用折叠和右值引用参数
当将一个左值传递给一个右值引用参数,且该右值引用参数指向模板类型参数时,编译器推断模板类型参数为实参的左值引用;
f3(i); //T是int&
f3(ci); //T是const int&
引用折叠只能用于间接创建引用的引用,如类型别名或模板参数;
f3<int&>(i); //函数的形参为 int & &&,且折叠为int &
即如果一个函数参数是指向模板类型参数的右值引用,则既可以传递左值也可以传递右值;
折叠规则:
X& &、X& &&、X&& &都会折叠为X&
X&& &&折叠为X&&
接受右值引用参数的模板函数:
template<class T>
void f(T&& t)
{
T tt = t; //拷贝还是绑定一个引用
}
6、理解std::move
可以用move获得绑定到左值的右值引用;
move的定义方式
template<class T>
typename remove_reference<T>::type && move(T &&t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
7、转发
如果一个函数参数是指向模板类型参数的右值引用,它与对应的const属性和引用属性将得到保持;
void f1(int &i) {}
void f2(int &&i) {}
template<class F, class T>
void ft(F f, T && t)
{
f(t);
}
int i = 1;
ft(f1, i); //正确,t的类型为int &
ft(f2, 1); //错误,右值引用无法绑定到右值引用变量
当用于一个指向模板类型参数的右值引用的函数参数时,forward会保持实参类型的所有细节;
template<class F, class T>
void ft(F f, T && t)
{
f(forward<T>(t));
}
int i = 1;
ft(f1, i);
ft(f2, 1); //正确,右值引用绑定到右值引用,不是右值引用变量
与std::move相同,不是using声明对std::forward是一个好主意;
三、模板与重载
对于一个调用,如果一个非模板函数与一个模板函数提供同样好的匹配,则选择非模板函数;
候选的函数模板总是可行的,因为模板实参推断会派出任何不可行的模板;
当有多个重载版本对一个调用提供同样好的匹配时,应选择最特例化的版本;
例如:const T & 和 T* ,前者可用于任何类型,也包括指针类型,但后者只能用于指针类型,所以如果实参为指针类型应选择后者版本;
在定义任何函数之前,记得声明所有重载的函数,这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需要的版本;
四、可变参数模板
//Args是一个模板参数包,表示0个或多个模板类型参数
//reset是一个函数参数包,表示0个或多个函数参数
template <class T, class ... Args>
void foo(const T &, const Args& ... reset) {}
int i; double d; string s;
foo(i, d, s); //包中有两个参数
foo(i, d); //包中有一个参数
foo(i); //空包
sizeof运算符,当需要知道包中有多少元素时,可以使用该运算符;
cout<<sizeof...(Args)<<endl;