模板与泛型编程
- 面向对象编程(oop)和泛型编程都能处理在编写程序时不知道类型的情况;不同之处:oop能处理类型在程序运行之前都未知的情况,而在泛型编程中,在编译时就能获知类型了
- 容器、迭代器和算法都是泛型编程的例子
- 模板是c++泛型编程的基础
- 一个模板就是一个创建类或函数的蓝图或公式
1.定义模板:
-
模板定义以关键字template开始,后跟一个模板参数列表(是一个逗号分隔的一个或多个模板参数的列表,不能为空)
函数模板:是一个公式,可用来生成针对特定类型的函数版本
//例:compare的模板版本 template <typename T> int compare(const T &v1, const T &v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; }
1.实例化函数模板:调用template时,编译器使用实参的类型来确定绑定到模板参数T的类型,之后编译器利用推断出来的模板参数来实例化一个特定版本的函数
- 类型参数前必须使用关键字class或typename(常使用typename)
非类型参数:表示一个值而非一个类型,通过一个特定的类型名而非关键字class或typename来指定
1.当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出来的值所代替,这些值必须是常量表达式
//例:非类型模板参数 template <unsigned N, unsigned M> int compare(const char (&p1)[N], const char (&p2)[M]) { return strcmp(p1,p2); }
2.非类型模板参数的模板实参必须是常量表达式
- 函数模板可以声明为inline或constexpr的,inline或constexpr说明符放在模板参数列表之后,返回类型之前
- 当我们实例化出模板的一个特定版本时,编译器才会生成代码; 函数模板和类模板成员函数的定义通常放在头文件中
类模板: 用来生成类的蓝图,与函数模板不同之处在于不能为类模板推断模板参数类型
//例:Blob类模板 template <typename T> class Blob { public: //... private: //... };
1.当使用一个类模板时,必须提供额外信息,这些额外信息是显示模板实参列表,它们被绑定到模板参数,编译器使用这些模板实参来实例化出特定的类
//使用显示模板实参实例化Blob类 Blob<int> ia; //空Blob<int> Blob<int> ia2 = {0, 1, 2, 3, 4}; //有5个元素的Blob<int>
2.一个类模板的每个实例都形成一个独立的类。实例化出来的Blob<int>与其他的Blob类型都没有关联,也不会对任何其他Blob类型的成员有特殊访问权限
3.一个类模板中的代码如果使用了另外一个模板,通常不将一个实例类型的名字用作其模板实参·,相反将模板自己的参数当作被使用模板的实参
//Blob中使用另外一个模板的数据成员 shared_ptr<vector<T>> data; //T为Blob模板的参数
4.在类外定义一个成员函数时,必须说明成员属于哪个类
//例:在类外定义成员函数check template <typename T> void Blob<T>::check(size_type i, const string &msg) const { if (i >= data->size()) throw out_of_range(msg); }
5.默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化
类模板和友元:
1.如果类模板包含一个非模板友元,则友元被授权可以访问所有模板实例;如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例
2.进行友元声明必须首先前置声明模板本身
//前置声明,在Blob中声明友元所需 template <typename> class BlobPtr; template <typename> class Blob; template <typename T> bool operator==(const Blob<T>&, const Blob<T>&); template <typename T> class Blob { //每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符 friend class BlobPtr<T>; friend bool operator==<T> (const Blob<T>&, const Blob<T>&); };
3.为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数
4.可以将模板参数类型声明为友元
- 可以定义typedef来引用实例化的类,还可以使用using来声明类型别名
typedef Blob<string> StrBlob; //引用实例化的Blob类
template <typename T> using twin = pair<T, T>; //twin为pair<T, T>的别名
twin<string> authors; //authors是一个pair<string, string>
- 类模版的static成员:每一个类模版的实例都有自己的static成员实例,但对于给定的类型,该static成员实例共享;类模版的static成员有且仅有一个定义
2.模板参数:
- 一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前
- 模板参数会隐藏外层作用域中声明的相同名字;在模板内不能重用模板参数名·
typedef double A;
template <typename A, typename B> void f(A a, B b)
{
A tmp = a;
double B; //错误:重声明模板参数B
}
- 模板声明必须包含模板参数,声明中的模板参数的名字不必与定义中相同:
template <typename T> T calc(const T&, const T&); //声明
//模板的定义
template <typename Type>
Type calc(const Type& a, const Type& b) { /*...*/}
- 当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class
- 在新标准中,我们可以为函数和类模板提供默认实参; 对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参
- 如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对
template <class T = int> class Numbers {
public:
Numners(T v = 0) : val(v) {}
private:
T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; //空<>表示我们希望使用默认类型
3.成员模板:一个类包含本身是模板的成员函数
普通类的成员模板:
//函数对象类,对给定指针执行delete class DebugDelete { public: DebugDelete(ostream &s = cerr) : os(s) { } template <typename T> void operator()(T *p) const { os << "deleting unique_ptr" << endl; delete p; } private: ostream &os; };
类模板的成员模板:类和成员有各自的、独立的模板参数
template <typename T> class Blob { template <typename It> Blob(It b, It e); //... };
当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供参数列表,类模板的参数列表在前,后跟成员自己的模板参数列表:
template <typename T> //类的类型参数 template <typename It> //构造函数的类型参数 Blob<T>::Blob(It b, It e): data(make_shared<vector<T>>(b, e)) {}
- 在哪个对象上调用成员模板,编译器就根据该对象的类型来推断类模板参数的实参
4.控制实例化+效率与灵活性:
控制实例化:
- 当模板被使用时才会实例化这一特性意味着,相同的实例可能出现在多个对象文件中
- 在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重,在新标准中,可以通过显示实例化来避免这种开销
extern template declaration; //实例化声明 template declaration; //实例化定义
- 当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extrern声明
- extern声明必须出现在任何使用此实例化版本的代码之前,其定义可以不放在本文件,但必须将定义的文件链接起来
- 一个类模板的实例化定义会实例化改模板的所有成员,包括内联的成员函数;因此,我们用来显示实例化一个类模板的类型,必须能用于模板的所有成员
效率与灵活性:
1.shared_ptr给予我们共享指针所有权的能力;unique_ptr则独享指针
2.传递给shared_ptr一个可调用对象即能重载其删除其;用户必须在定义unique_ptr时以显示模板实参的形式提供删除器的类型
3.unique_ptr在编译时绑定删除器,shared_ptr在运行时绑定删除器
4.通过在编译时绑定删除器,unique_ptr避免了间接调用删除器的运行时开销;通过在运行时绑定删除器,shared_ptr使用户重载删除器更为方便
5.模板实参推断:从函数实参来确定模板实参
- 在实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配
类型转换与模板类型参数:
1.只有很有限的几种类型转换会自动地应用于这些实参
- const转换:将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参
- 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换
2.如果函数参数类型不是模板参数,则对实参进行正常的类型转换
函数模板显示实参:
1.指定显示模板实参:
- 定义表示返回类型的第三个模板参数,从而允许用户控制返回类型
template <typename T1, typename T2, typename T3> T1 sum(T2, T3); //当实例化时,传入的函数实参不能推断出函数的返回类型,因此每次调用时必须提供一个显示模板实参
- 显示模板实参按由左至右的顺序与对应的模板参数匹配;只有尾部参数的显示模板实参才可以忽略
2.对于模板类型参数已经显示指定了的函数实参,也进行正常的类型转换
尾置返回类型与类型转换:
当希望用户确定返回类型时,用显示模板实参很有效;但在其他情况下,要求显示模板实参会给用户增添额外负担
1.尾置返回类型:返回值是序列中的某个元素,使用尾置返回类型,函数类型可以用auto替代
template <typename It> auto fcn(It beg, It end)-> decltype(*beg) { //处理序列 return *beg; //返回序列中一个元素的引用 }
2.对于上述模板,为了得到元素类型,可以使用标准库的类型转换模板。模板定义在头文件type_traits(p606、表16.1)
- 组合使用模板参数的成员、尾置返回及decltype,就可以在函数中返回元素值的拷贝
template <typename It> auto fcn(It beg, It end) - > typename remove_reference<decltype(*beg)>::type { //处理序列 return *beg; //返回序列中一个元素的拷贝 }
函数指针和实参推断:
1.当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值,编译器使用指针的类型来推断模板实参
template <typename T> int compare(const T&, const T&); //pf1指向实例int compare(const int&, const int&) int (*pf1)(const int&, const int&) = compare;
2.当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值
模板实参推断和引用:
1.当函数参数是模板类型参数的一个普通(左值)引用时,只能传递给它一个左值;如果实参是const的,则T将被推断为const类型
template <typename T> void f1(T&); //实参必须是一个左值
2.当函数参数是const T&,则可以传递给它任何类型的实参(一个对象、一个临时对象或一个字面值常量); 函数参数本身是const,T推断的结果不会是一个const类型
template <typename T> void f2(const T&); //可以接受一个右值
3.当函数参数是一个右值引用时,可以传入左值或右值,左值(假设为int)会将T绑定为int&,右值则绑定为int
template <typename T> void f3(T&&);
4.两个例外规则
- 第一个例外规则影响右值引用参数的推断,因为传入左值(int),T被推断为int&,意味着f3的函数参数应该是一个类型int&的右值引用,但通常我们不能直接定义一个引用的引用
- 第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”
- X& &、X& &&和X&& &折叠成类型X&; 类型X&& &&折叠成X&&
- 引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
5.在实际情况中,右值引用通常用于两种情况:模板转发其实参或模板被重载
转发:
1.某些函数需要将其一个或多个实参连同类型不变地转发给其他函数(需要保持被转发实参的所有性质)
2.如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值/右值属性将得到保持
template <typename F, typename T1, typename T2> void flip2(F f, T1 &&t1, T2 &&t2) { f(t2, t1); }
3.在调用中使用std::forward保持类型信息(forward定义在头文件utility中)
- forward返回该显示实参类型的右值引用,即,forward<T>的返回类型是T&&
- 通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性
template <typename F, typename T1, typename T2> void flip2(F f, T1 &&t1, T2 &&t2) { f(std::forward<T2>(t2), std::forward<T1>(t1)); }
6.重载与模板:
- 函数模板可以被另一个模板或一个普通非模板函数重载;名字相同的函数必须具有不同数量或类型的参数
函数匹配规则:若模板被重载,则函数的匹配会发生变化
1.对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例
2.可行函数按类型转换来排序,需要类型转换的排在不需要转换的后面
3.如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数;但如果有多个函数提供同样好的匹配,则:
- 其中只有一个非模板函数,则选择此函数
- 其中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板
- 否则,调用有歧义
- 在定义任何函数之前,记得声明所有重载的函数版本。
7.可变参数模板:
- 一个可变参数模板就是一个接受可变数目参数的模板函数或模板类
- 可变数目的参数被称为参数包,存在两种:模板参数包、函数参数包
- 用一个省略号来指出一个模板参数或函数参数表示一个包(class...或typename...)
- 当需要知道包中有多少元素时,可以使用sizeof...运算符
template <typename...Args> void g(Args...args){
cout << sizeof...(Args) << endl; //类型参数的数目
cout << sizeof...(args) << endl; //函数参数的数目
}
编写可变参数函数模板
1.可使用一个initializer_list来定义一个可接受可变数目实参的函数
2.可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后用剩余实参调用自身
//例:定义可变参数版本的print template <typename T> ostream &print(ostream &os, const T &t) { return os << t; //包中最后一个元素之后不打印分隔符 } template <typename T, typename...Args> ostream &print(ostream &os, const T &t, const Args&...rest) { os << t << ", "; //打印第一个实参 return print(os, rest...); //递归调用,打印其他实参 }
为了终止递归,定义了一个非可变参数的print函数,接受一个流和一个对象
包扩展:
1.对于一个参数包,除了获取其大小,能做的唯一的事情就是扩展;当扩展一个包时,还要提供用于每个扩展元素的模式
2.扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表
template <typename T, typename...Args> ostream &print(ostream &os, const T &t, const Args&...rest) //扩展Args { os << t << ", "; return print(os, rest...); //扩展rest } //此模式为print调用生成实参列表
3.对参数的函数进行扩展,注意省略号的位置,不是对函数参数的扩展
template <typename...Args> ostream &errorMsg(ostream &os, const Args&...rest) { return print(os, debug_rep(rest)...); }
8.模板特例化:
- 当我们不能(或不希望)使用模板版本时,可以定义类或函数版本的一个特例化版本
- 一个特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型
- 当我们特例化一个函数模板时,必须为原模版中的每个模板参数都提供实参
//compare的特殊版本,处理字符数组的指针
template <>
int compare(const char* const &p1, const char* const &p2)
{
return strcmp(p1, p2);
}
- 特例化的本质是实例化一个模板,而非重载它,因此,特例化不影响函数匹配(不是一个非模板的独立函数)
- 模板及其特例化版本应该声明在同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本
- 一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参
//原始的、最通用的版本
template <class T> struct remove_reference {
typedef T type;
};
//部分特例化版本,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{
typedef T type;
};
template <class T> struct remove_reference<T&&> //右值引用
{
typedef T type;
};
- 部分特例化版本的参数列表是原始模板的参数列表的一个子集或者是一个特例化版本
- 我们可以只特例化特定成员函数而不是特例化整个模板