- 一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种函数称为成员模板。注意!成员模板不能是虚函数。
普通(非模板)类的模板成员函数
例子:我们定义一个普通类,将定义一个重载的函数调用运算符,接收一个指针,并对此指针指向delete
由于希望删除器能够使用于任何类型,因此定义为模板
//函数对象类,对给定指针指向delete
class DebugDelete
{
public:
DebugDelete(std::ostream &s = std::cerr) os(s) { }
//于任何模板相同,T的类型由编译器推断
template <typename T> void operator()(T *p) const
{
os << "deleting unique_ptr" << std::endl;
delete p;
}
private:
std::ostream &os;
};
double* p = new double;
DebugDelete d;
d(p); //通过创建对象调用成员模板T推断为duoble
int* ip = new int;
DebugDelete()(ip); //通过一个临时DebugDelete对象上调用operator()(int*)
类模板的成员模板
在此情况下,类和成员各自有自己的模板参数
template <typename T>
class Blob
{
template <typename It> Blob(It a, It b);
//...
};
在类模板外部进行其成员模板的函数体定义时,需要同时为类模板和成员模板提供模板参数列表。
template <typename T>
template <typename It>
Blob<T>::Blob(It a, It b) : data(std::make_shared<vector<T>>(a, b)) { }
实例化与成员模板
int ia[] = {1,2,3,4,5};
vector<long> vi = {1,2,3,4,5};
list<const char*> w = {"one", "two", "three"};
//实例化Blob<int>类及几首两个int*参数的构造函数
Blob<int> al(begin(ia), end(ia));
//实例化Blob<int>类的接收vector<long>::iterator的构造函数
Blob<int> a2(vi.begin(), vi.end());
- 显式实例化——由于模板被使用时才会被实例化,但是有时候相同的实例可能在不同的对象文件中会使用到,当多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数,每个文件中就都会有一个模板的实例,所产生的开销将会很大。
在新标准中,可以通过显式实例化来避免这种开销。形式如下:
extern template declaration; //实例化声明
template declarartion; //实例化定义
declaration是一个类或函数声明,其中模板参数已被替换为模板实参:
//实例化声明与定义
extern template class Blob<string>; //声明
template int comare(const int&, const int&); //定义
编译器遇上extern模板声明时,不会再本文件中实例化。同时,将一个实例化声明为extern表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。对于一个特定版本的实例化,可能有多个extern,但必须只有一个定义。
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。
普通类模板的实例化是使用哪个成员函数便实例化哪个成员函数。
因此,我们用来显式实例化一个模板的类型,必须能用于模板的所有成员。
- 类型转换与模板类型参数。将实参传递给带模板类型的函数形参时,能够自动应用类型转换的只有const转换及数组或函数到指针的转换。
const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转化为一个指向其手元素的指针。类似,一个函数实参可以转化为一个指向该函数类型的指针。
例如:
template <typename T> T fobj(T, T); //实参将被拷贝
template <typename T> T fref(const T&, const T&); //引用
string s1("a value");
const string s2("another value");
fobj(s1, s2); //调用fobj(string, string);const被忽略
fref(s1, s2); //调用fref(const string&, const string&);将s1转化为const是允许的
int a[10], b[20];
fobj(a, b); //调用fobj(int*, int*)
fref(a, b); //错误,对于一个引用形参参数来说,数组不会转化为指针。
- 函数模板显式实参。有时候希望允许用户控制模板的实例化,可以指定显式模板实参。
例如,定义一个sum函数,允许用户指定返回结果类型来控制精度
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3); //没有任何实参类型给编译器去推断T1的类型
//因此每次调用sum时都必须为T1提供一个显式模板实参
auto val3 = sum<long long>(i, lng); //通过指定和实参,编译器推断为long long sum(int, long)
##注意##
显式模板实参按从左向右的顺序与对应的模板参数匹配。只有尾部的显式模板实参才可以忽略。
//糟糕的设计:用于必须指定所有三个模板参数
template <typename T1, typename T2, typename T3>
T3 sum(T1, T2);
auto val2 = sum<long long, int, long>(i, lng);
- 除了用户指定模板实参类型,可以选择通过尾置返回类型来判定。
例如,当返回一个迭代中某个元素的引用,但我们并不知道该迭代器中存储的类型
必须由编译器推断
template <typename It>
??? &fcn(It beg, It end)
{
//处理序列
return *beg; //返回序列中一个元素的引用
}
在使用时并不知道返回结果的准确类型
vector<int> vi = {1,2,3,4,5};
Blob<string> ca = {"hi", "Blob"};
auto &i = fcn(vi.begin(), vi.end()); //需要返回int&
auto &s = fcn(ca.begin(), ca.end()); //需要返回string&
使用尾置来声明返回类型
template <typename T>
auto fcn(T beg, T end) -> decltyp(*beg)
{
//处理序列
return *beg; //返回一个元素的引用
}
- 进行类型转化的标准库模板类
有时候,我们希望返回一个元素的值,而并非引用。
为了获得元素类型,我们可以使用标准库的类型转换模板,这些模板定义在头文件type_traits中。
//为了使用模板参数的成员,必须typename
template <typename T>
auto fcn2(T beg, T end) -> typename remove_reference<decltype(*beg)>::type
{
//处理序列
return *beg; //返回序列中一个元素的引用
}
- 当参数类型无法确定模板实参的唯一类型,必须显式指出实例化哪个类型。
template <typename T> int compare(const T&, const T&);
//pf1指向实例int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
pf1中参数的类型决定了T的模板实参的类型。例子中T的模板实参类型为int。
指针pf1指向compare的int版本
当出现重载版本,每个版本接收一个不同的函数指针类型,则需要显式指出实例化哪个版本
//func重载
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); //错误用法,产生歧义,无法确定使用哪个版本的compare
//正确用法
func(compare<int>); //显式指出T为int类型,即compare(const int&, const int&)
- 如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。
即,对于一个给定类型X:
X& &、X& &&和X&& &都折叠成类型X&
类型X&& &&折叠成X&&
引用折叠只能应用于间接创建的引用的引用,类似类别名或模板参数
引用折叠产生的效果是,我们可以将任意类型的实参传递给T&&类型的函数参数。
如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被绑定到一个左值;
如果传递给的实参是一个左值,那么函数则会实例化成一个普通的左值引用(T&)
- 通过使用引用折叠,我们可以定义保持类型信息的函数参数
某些函数需要将其一个或多个实参连同类型不变的转发给其他函数。
//接收一个可调用对象和另外两个参数的模板
//版本一
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}
版本一在一般情况下可以较好的工作,但当我们希望能用它调用一个接收引用的参数时,就会出现问题:
void f(int v1, int &v2)
{
cout << v1 << " " << ++v2 << endl;
}
这段f函数,改变了绑定到v2上的实参的值;
但是,通过flip1调用f时,f所作的改变不会影响到实参
f(42, i); //f改变了实参i
flip1(f, j, 42); //通过flip1调用f不会改变j
#原因#
从模板参数列表中可以看到,由实参42推断出T为int类型,于是j被传递过去的是int而不是int&。
模板被实例化为void flip1(void(*fcn)(int, int&), int t1, int t2);
//版本二
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}
此处模板参数列表为右值引用,如果我们调用flip2(f, j, 42),则会传递给参数t1一个左值j
但是,在flip2中,推断出的T1类型为int&,意味着t1的类型会折叠为int&。
由于是引用类型,t1被绑定到j上,flip2调用f时,f的引用参数v2也绑定到t1,即绑定到f上。
因此f递增v2也同时改变了j的值。
#Note#
如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值/右值属性可以得到保持
由于flip2解决了一般问题。它对于接收一个左值引用的函数工作的很好,但不能接收右值引用参数的函数
即:
void f(int &&i, int &j)
{
cout << i << " " << j << endl;
}
当试图通过flip2调用g,则参数t2将被传递给g的右值引用参数。
flip2(g, j, 42); //错误,不能从一个左值实例化int&&
传递给g的是flip2中的t2.函数参数与其他任何变量都一样,都是左值表达式。
因此flip2对g的调用将传递给原本需要一个右值的形参传递一个左值给它
//版本三
使用forward的新标准库来爆出原始实参类型,类似move,forward定义在头文件utility中
forward必须通过显式模板实参来调用,forward返回该显式实参类型的右值引用。即
forward<T> 返回类型为T&&
//重写flip
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
#Note#
与std::move一样,使用时不用using声明,直接加上std::使用,std::forward