c++--模板与泛型编程

专题--泛型编程的基础(模板)

  面向对象编程(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 }
View Code

原文地址:https://www.cnblogs.com/cygalaxy/p/6881868.html