专题--泛型编程的基础(模板)
面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:OOP能处理类型在程序运行之前都未知的情况(动态绑定);而泛型编程中,在编译时就能获知类型了。模板是泛型编程的基础。
一、 函数模板
1. 适用情况:如果两个函数几乎是相同的,唯一的差异是参数的类型,函数体则完全一样。
2. 定义
template <模板参数列表(以逗号分隔)>
1 template <typename T> 2 int compare(const T &v1,const T &v2) 3 { 4 if (v1<v2) return -1; //假设类型T支持<操作 5 if (v2<v1) return 1; 6 }
3. 实例化函数模板
当调用一个函数模板时,编译器用函数实参推断模板实参。
4. 模板参数类型
类型参数T的用途:指定返回类型,指定函数参数类型,在函数体内用于变量声明,变量类型转换
5. 非类型模板参数
一个非类型参数表示一个值而非一个类型。当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替,这些值必须是常量表达式。
用途:在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。
//编写一个conpare版本处理字符串字面常量。 //由于不能拷贝一个数组,所以我们将参数定义为数组的引用 template <unsigned N,unsigned M> int compare(const char (&p1)[N],const char (&p2)[M]) { return strcmp(p1,p2); } //调用compare compare("hi","mom"); //编译器在字符串字面常量末尾插入一个空字符作为终结符,因此编译器实例化出如下版本 int compare(const char (&p1)[3],const char (&p2)[4])
6. inline和constexpr的函数模板
template <typename T> inline T min(const T&,const T&) //注意inline位置
7. 编码原则
compare函数编写泛型代码的原则:模板中的参数是const 的引用;函数体中的条件判断仅使用<比较运算;
一个原则:模板程序应该尽量减少对实参类型的要求。
8. 模板编译
注意:
1)只有当我们实例化出模板的一个特定版本时,编译器才会生成代码;
2)函数模板和类模板成员函数的定义通常放在头文件中;
3)大多数编译错误发生在实例化期间报告。
保证传递给模板的实参支持模板所要求的的操作,以及这些操作能正确工作,是调用者的责任。
二、类模板
与函数模板不同之处是,编译器不能为类模板推断参数类型。为了使用类模板,必须在模板名后的< >中提供额外信息(显式模板实参)。
1. 用途:与类不同,模板可以用于更多类型的元素。
2. 定义
template <typename T> class Blob { public: typedef T value_type; typedef typename std::vector<T>::size_type size_type; Blob(); T& back(); T& operator[](size_type i); private: std::shared_ptr<std::vector<T>> data; void check(size_type i,const std::string &msg) const;
}
3. 实例化类模板
与标准库容器(STL)相同,使用Blob时,用户需要指出元素类型。也就是需要提供显式模板实参。
Blob<int> ia;
Blob<int> ia2={0,1,2,3,4};
Blob<string> names;
Blob<double> price;
一个类模板的每个实例都形成一个独立的类,类型Blob<string>与任何其他Blob类型都没有关联,也不会对任何其他Blob类型的成员有特殊访问权限。
4. 在类模板作用域中引用模板类型
类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。无论何时使用模板都必须显式提供模板实参。但这一规则有一个例外,在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。
5. 类模板的成员函数
注意:定义在类模板之外的成员函数,必须以关键字template开始,后接类模板参数列表:
1 //template <typename T> 2 //ret-type Blob<T>::member-nam(parm-list) 3 4 template <typename T> 5 void Blob<T>:;check(size_type i,const std::string &msg) const 6 { 7 if (i>=data->size()) 8 throw std::out_of_range(msg) 9 }
6. 类模板成员函数的实例化
成员函数只有在被用到时才进行实例化,这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。但是,实例化定义(template declaration)则不同(C++ Primer5 P598),实例化定义会实例化所有成员。
当模板被使用时才会进行实例化,这一特性意味着,相同的实例可能出现在多个对象文件中。在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化来避免这种开销。一个显式实例化有如下形式:(分别放在不同的两个.cpp文件)
extern template declaration; //实例化声明
template declaration; //显式实例化定义
declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参。例如:
extern template class Blob<string> //声明
template int compare(const int&,const int&); //定义
当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺程序在其他位置有该实例化的一个非extern定义。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
7. 类模板和友元
(C++ Primer P588)
1 //func.h 2 #ifndef __FUNC_H__ 3 #define __FUNC_H__ 4 5 /*对于一个模板类C*/ 6 template <typename> class Pal; //类模板Pal的前置声明 7 template <typename T> 8 class C 9 { 10 //C的每个实例将相同实例化的Pal声明为友元.类模板Pal必须有前置声明 11 friend class Pal<T>; //相同实例(T) 12 //类模板Pal2的所有实例都是C的每个实例的友元。不需要前置声明,友元声明中必须使用与类模板本身不同的模板参数X 13 template <typename X> friend class Pal2; //所有实例 14 //Pal3是普通类(非模板类),它是C所有实例的友元 15 friend class Pal3; 16 }; 17 18 /***************************************************/ 19 20 /*对于一个普通类(非模板类)C2*/ 21 template <typename T> class PP; //前置声明 22 class C2 23 { 24 //用类C2实例化的Pal是C2的一个友元。需要前置声明 25 friend class PP<C2>; //特定实例 26 //PP2的所有实例都是C2的友元。无需前置声明 27 template <typename T> friend class PP2; //所有实例 28 }; 29 30 #endif // __FUNC_H__ 31 32 //main.cpp 33 #include <iostream> 34 #include "func.h" // 35 using namespace std; 36 37 int main() 38 { 39 return 0; 40 }
总结:
1. "特定实例"或"相同实例"作为友元时,必须前置声明。
2. "所有实例"成为友元时,不需要前置声明。
3. 为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数(如 X)。
8. 模板类型别名(感觉用处不大)
1) typdef引用实例化的类: typedef Blob<string> StrBlob;
2) 为类模板定义一个类型别名
template <typename T> using twin=pair<T,T>; //一组类的别名
twin<double> authors; //pair<double,double> authors;
twin<int> win_loss; //pair<int,int> win_loss;
3) 定义一个模板类型别名时,可以固定一个或多个模板参数:
template <typename T> using partNo=pair<T,unsigned> //固定一个参数unsigned
partNo<string> books; //pair<string> books;
partNo<Student> kids; //pair<Student> kids;
9. 类模板的static成员
模板类的每个static成员必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的static对象(也就是相同类型对象共用同一个static成员)。类似任何其他成员函数,一个static成员函数只有在使用时才会被实例化。
1 template <typename T> 2 class Foo 3 { 4 public: 5 static std::size_t count() {return str;} 6 private: 7 static std::size_t ctr; 8 } 9 10 //实例化static成员Foo<string>::ctr和Foo<string>::count 11 Foo<string> fs; 12 //所有3个对象共享相同的Foo<int>::ctr和Foo<int>::count成员 13 Foo<int> fi,fi2,fi3;
三、模板参数
1. 两条原则:
1) 模板参数会隐藏外层作用域中声明的相同名字;
2) 与大多数其他上下文不同,在模板内不能重用模板参数名;
1 typedef double A; 2 template <typename A,typename B> 3 void f(A a,B b) 4 { 5 A tmp=a; //tmp的类型为模板参数A的类型 6 double B; //错误:重声明模板参数B 7 }
2. 使用类的类型成员
默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。因此,如果希望使用一个模板类型参数的类型成员,就必须显式告诉编译器改名字是一个类型。只能通过关键字typename来实现这一点。
1 template <typename T> 2 typename T::value_type top(const T& c) 3 { 4 if (!c.empty()) 5 return c.back(); 6 else 7 return typename T::value_type(); 8 } 9 10 11 //区别: 12 //T::value_type 访问静态成员 13 //typename T::value_type 访问类型成员
四、成员模板
成员模板不能是虚函数。
1. 普通类(非模板)的成员模板
2. 类模板的成员模板
在类模板外定义一个成员模板时:必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表:
template <typename T> //类的类型参数 template <typename It> //构造函数的类型参数 Blob<T>::Blob(It b,It e):data(std::make_shared<std::vetor<T>>(b,e)){}
五、模板实参推断
从函数实参来确定模板实参的过程被称为模板实参推断。
1. 类型转换与模板类型参数
C++中类型转换有以下几种:
1) const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
2) 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。
3) 算术转换:运算符的运算对象将转换成最宽的类型。(c++ primer P142)
4) 派生类向基类的转换:派生类向基类的隐式转换(包括动态绑定)。(c++ primer P530)
5) 用户定义的转换:类型转换函数的一般形式 "operator type() const;" (c++ primer P263&&P514)
其中,只有很有限的几种类型转换会自动地应用于函数实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例:与往常一样,顶层const无论在形参还是实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括如下两项:
1) const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
2) 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。
2. 函数模板显式实参
当函数返回类型与参数列表中任何类型都不相同时,经常会出现以下两种情况:
1) 在某些情况下,编译器无法推断出模板实参的类型;
2) 在一些情况下,我们希望允许用户控制模板实例化。
举例:
1 //编译器无法推断T1,它未出现在函数参数列表中 2 template <typename T1,typename T2,typename T3> 3 T1 sum(T2,T3) 4 //T1是显式指定的,T2和T3是从函数实参类型推断而来的 5 auto val3=sum<long long>(i,lng); //long long sum(int,long)
显式模板实参按由左至右的顺序与对应的模板参数匹配:第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,一次类推。只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数中推断出来。
1 template <typename T1,typename T2,typename T3> 2 T3 sum(T2,T1); 3 4 //错误:不能推断前几个模板参数 5 auto val3=sum<long long>(i,lng); 6 //正确:显式指定了所有三个参数 7 auto val2=sum<int,long,long,long>(i,lng);
3. 尾置返回类型与类型转换
用途:当返回类型不确定时。
1) 尾置返回类型
举例:我们希望编写而一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用:由于尾置返回出现在参数列表之后,它可以使用函数的参数。
//尾置返回类型 template <typename It> auto fcn(It beg,It end) ->decltype(*beg) { //处理序列操作 return *beg; //返回序列中一个元素的引用 } //decltype(*beg) 不能写在auto 的位置,是因为在参数列表之前,beg 都是不存在的。
2) 进行类型转换的标准库模板类
所需头文件 #include<type_traits>
//remove_reference::type脱去引用,剩下元素类型本身 //为了使用模板参数的成员,必须使用typename template <typename It> auto fcn2(It beg,It end) -> typename remove_reference<decltype(*beg)>::type { //处理序列 return *beg } //typename 告诉编译器,type表示一个类型
4. 函数指针和实参推断
当用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
1 template <typename T> int compare(const T&,const T&); 2 //pf1指向实例int compare(const int&,const int&); 3 int (*pf1)(const int&,const int&)=compare; 4 /****************************************/ 5 //func的重载版本 6 func(int (*)(const int&,const int&)); 7 func(int (*)(const string&,const string&)); 8 //错误:使用compare的哪个实例 9 func(compare); 10 //正确:显式指出实例化哪个版本 11 func(compare<int>);
略(c++ primer P607)
5. 模板实参推断和引用(比较绕)
两个正常绑定规则:
5.1 从左值引用函数参数推断类型
1. 当一个函数参数是模板类型参数(T) 的一个普通(左值)引用时(T&),绑定规则告诉我们,只能传递给它一个左值(如一个变量或一个返回引用类型的表达式)。实参可以是const类型,也可以不是。
2. 如果一个函数参数的类型是const T&,可以传递给它任何类型的实参--一个对象(const或非const)、一个临时对象或是一个字面值常量值。
//1. template <typename T> void f1(T&); //实参必须是一个左值 f1(i); //i是一个int;模板参数类型T是int f1(ci); //ci是一个const int;模板参数T是const int f1(5); //错误:传递给一个&参数的实参必须是一个左值 //2. template <typename T> void f2(const T&); //可以接受一个右值 f2(i); //i是一个int;模板参数T是int f2(ci); //ci是一个const int,但模板参数T是int f2(5); //一个const & 参数可以绑定道一个右值;T是int
5.2 从右值引用函数参数推断类型
当一个函数参数是一个右值引用(T&&)时,可以传递给它一个右值。
1 template <typename T> 2 void f3(T&&); 3 4 f3(42); //实参是一个int;模板参数T 是int
两条例外规则
C++在正常绑定规则之外定义了两个例外规则,允许一个右值引用绑定到一个左值上。这连个例外规则是move这种标准库设施正确工作的基础。
1. 第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值(如i )传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断推断模板类型参数为实参的左值引用类型。因此,当我们调用f3(i)时,编译器推断T的类型为int&,而非int。
2. 第二个规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除第一个例外),引用会折叠成一个普通的左值引用类型。在新标准下,折叠规则扩展到右值引用。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即对于一个给定类型X:
1) X& &、X& &&和X&& &都折叠成类型X&;
2) 类型X&& &&折叠成X&&。
这两个规则导致了两个重要结果:
1. 如果一个函数参数是 "T&&",则它可以被绑定到一个左值;且
2. 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&)。
1 template <typename T> 2 void f3(T&& val) 3 { 4 T t=val; //拷贝还是绑定一个引用 5 t=100; 6 if(val==t) 7 cout<<"绑定一个引用"<<endl; 8 else 9 cout<<"拷贝"<<endl; 10 } 11 12 int main() 13 { 14 int i=10; 15 f3(i); //推断T为int&,因此T&&是int& &&,会折叠为int& 16 f3(10); //推断T为int 17 return 0; 18 }
注意15行注释:推断T为int&,因此T&&是int& &&,会折叠为int&.
在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。使用右值引用的函数模板通常使用(P481)中介绍的方式进行重载:
1 template <typename T> void f(T&&); //绑定到非const右值 2 template <typename T> void f(const T&); //绑定到左值和const右值
略(c++ primer P608)
六、重载与模板
函数模板可以被另一个模板或一个普通非模板函数重载。与往常一样,名字相同的函数必须具有不同数量或类型的参数。
函数匹配规则:《C++ primer》 P615,值得注意的是,如果有多个函数提供同样好的匹配,则:
--若同样好的函数中只有一个是非模板函数,则选择此函数;(同等条件下,优先选非模板函数)
--若同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板;(同等条件下,选择更特例化的函数模板)
--否则,此调用有歧义。
1 #include <iostream> 2 #include <sstream> 3 using namespace std; 4 5 template <typename T> 6 string debug_rep(const T &t) 7 { 8 cout<<"debug_rep版本"<<endl; 9 ostringstream ret; 10 ret<<t; 11 return ret.str(); 12 } 13 template <typename T> 14 string debug_rep(T *p) 15 { 16 cout<<"debug_rep1版本"<<endl; 17 ostringstream ret; 18 ret<<"pointer:"<<p; //打印指针本身的值 19 if(p) 20 ret<<" "<<debug_rep(*p); 21 else 22 ret<<"null pointer"; 23 return ret.str(); 24 } 25 //非模板和模板重载 26 string debug_rep(const string &s) 27 { 28 cout<<"非模板函数版本"<<endl; 29 return '"'+s+'"'; // 30 } 31 int main() 32 { 33 string s("hello"); 34 cout<<debug_rep(s)<<endl<<endl; 35 cout<<debug_rep(&s)<<endl<<endl; 36 37 const string *sp=&s; 38 cout<<debug_rep(sp)<<endl<<endl; //多个函数模板,优先匹配"更特例化的模板" 39 40 string s1("hi"); 41 cout<<debug_rep(s1)<<endl<<endl; //同等条件下,优先匹配"非模板函数" 42 return 0; 43 }