当前位置:首页 » 《随便一记》 » 正文

【 C++私房菜】模板的入门与进阶

13 人参与  2024年02月12日 12:11  分类 : 《随便一记》  评论

点击全文阅读


目录

一、模板的定义

a.函数模板的调用

b.类模板的定义

2、模板的重载

3、非类型模板参数和模板类型参数

4、模板的编译

二、模板的特化

1、函数模板特化

2、类模板特化

a.全特化

b.偏特化

三、模板相关定义


一、模板的定义

a.函数模板的调用

理在的 C+编译器实现了 C++新增的一项特性——函数模板(function template)。函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如 int、double) 替换。通过将类型作为参数传递给模板。可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序。因此有时也被称为通用编程,由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。下面介绍为何需要这特性以及其工作原理。

C++的函数模板功能可以自动完成一个节省时间,且可靠的过程。函数模板允许以任意类型的方式来定义函数。一个函数模板就是一个公式,可以用来生成针对特定类型的函数模板。例如,可以这样建立一个交换模板:

 template<typename AnyType> //template<class AnyType> void Swap(AnyType &a,AnyType &b) {     AnyType t = a;     a = b;     b = t; }

第一行指出,要建立一个模板,并将类型命名为 AnyType。关键字 template 和 typename 是必需的,也可以使用关键字 class 代替 typename。另外,必须使用尖括号。类型名可以任意选择 (这里为 AnyType),只要遵守C++命名规则即可。许多程序员都使用简单的名称,如 T。余下的代码描述了交换两个 AnyType值的算法。模板并不创建任何函数,而只是告诉编译器如何定义函数。需要交换int的函数时,编译器按模板模式创建这样的函数,并用 int 代 AnyType。同样,需要交换double 的函数时,编译器将模板模式创建这样的函数,并用 double 代 AnyType。

提示:如果需要多个将同一种算法用于不同类型的函数,请使用模板。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字typename 而不是使用class。

注意,函数模板不能缩短可执行程序。最终仍将由独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单、更可靠。

更常见的情形是,将模板放在头文件中,并在需要使用模板的文件中包含头文件。 注:在模板定义中,不能将模板参数列表置空。


b.类模板的定义

类模板(class template)是用来生成类蓝图的。与函数模板不同的是,编译器不能为类模板推断模板类型参数。

类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。在类模板(及其成员)的定义中,我们将模板参数当作替身,代替使用模板时用户需要提供的类型或值。

 template<class T1, class T2, ..., class Tn> class 类模板名 {  // 类内成员定义 }; // 动态顺序表 // 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具 template<class T> class Vector { public:     Vector(size_t capacity = 10)         : _pData(new T[capacity])         , _size(0)         , _capacity(capacity)     {}     // 使用析构函数演示:在类中声明,在类外定义。     ~Vector();     void PushBack(const T& data);     void PopBack();     // ...     size_t Size() { return _size; }     T& operator[](size_t pos){         assert(pos < _size);         return _pData[pos];     }      private:     T* _pData;     size_t _size;     size_t _capacity; }; // 注意:类模板中函数放在类外进行定义时,需要加模板参数列表 template <class T> Vector<T>::~Vector() {     if (_pData)         delete[] _pData;     _size = _capacity = 0; }

当使用一个类模板时,我们必须提供额外信息。我们现在知道这些额外信息是显式模板实参(explicit template argument)列表,它们被绑定到模板参数。编译器使用这些模板实参来实例化出特定的类。例如 vector<int> v;当我们的vector模板实例化一个类时,它会重写vector模板,将模板参数T的每一个实例都替换为给定的模板实参。

一个类模板的每个实例都形参一个独立的类。类型vector<int>与任何其他vector类型都没有关联,也不会对任何其他vector类型的成员由特殊访问权限。

如果一个成员函数没有被使用,则它不会被实例化。成员函数只有在被用到时才进行实例化,这一特性使得即使某这类型不能完全符合模板操作的要求,我们仍能使用该类型实例化类。

默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。

在类代码内简化模板类名的使用:当我们使用一个模板类时必须提供模板实参,但如果在类模板自己的作用域中,我们可以直接使用模板名而不提供实参:

 template < typename T>  class BlobPtr { public:     BlobPtr& operator++(); //前置++     BlobPtr& operator--(); //前置--     //...     }

当我们处于一个类模板的作用域内时,编译器处理模板自身引用时就好像我们以及提供了与模板参数匹配的实参一样。即,就好像这样编写代码一样: BlobPtr<T>& operator++()

在类模板外使用模板类名:当我们在类模板外定义其成员时,我们并不在类的作用域中,直到遇到类名才进入类的作用域。:

 template < typename T>  BlobPtr<T> BlobPtr<T>::operator++(int)//后置++ {     BlobPtr ret=*this;     ++*this;     return ret; }

由于返回类型位于类的作用域开,我们必须指出返回类型是一个实例化的BlobPtr,它所用类型与类实例化所用类型一致。在函数体内,我们已经进入类的作用域,因此在定义ret时无须重复模板实参。如果不提供模板实参,则编译器将假定我们使用的类型与成员实例化所用类型一致。

在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。

类模板中的static成员:对于任何其他static数据成员相同,模板类的每一个static数据成员必须由且仅有一个定义。但是,类模板的每个实例都有一个独有的static对象。因此与定义模板的成员函数相似,我们将static数据成员也定义为模板:

 template<class T> size_t Foo<T>::ctr = 0;

类似其他任何成员函数,一个static成员函数只有在使用时才会实例化。


2、模板的重载

需要多个对不同类型使用同一种算法的函数时,可使用模板。然而,并非所有的类型都使用相同的算法。为满足这种需求,可以像重载常规函数定义那样重载模板定义。和常规重载一样,重载的模板的函数特征标必须不同。例如新增一个交换模板,用于交换两个数组中的元素。原来的模板的特征标为(T &,T&),而新模板的特征标为(T ,T,int)。注意,在后一个模板中,最后一个参数的类型为具体类型(int),而不是泛型。因此,并非所有的模板参数都必须是模板参数类型。

 template<typename AnyType> void Swap(AnyType &a,AnyType &b,int c); ​ template<typename AnyType> void Swap(AnyType &a,AnyType &b);

3、非类型模板参数和模板类型参数

模板参数分类类型形参与非类型形参。

类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。 非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

在我们的Swap函数中有一个模板类型参数(type parameter)。一般来说,我们可以将类型参数看作类型的说明符,就像内置类型或类类型说明符一样使用。特别的是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于声明变量或类型转换:

 template<class T> T foo(T* p) {     T tmp=*p; //tmp的类型将是p指向的类型。     //...     return tmp; }

类型参数前必须使用关键字class或template。在模板参数列表中,这两个关键字的含义相同,可以互换使用。一个模板参数列表中可以同时使用这两个关键字。

除了定义类型参数,还可以在模板中定义非类型参数(nontype parameter)。一个非类型模板参数表示一个值而非一个类型。我们通过一个特定的类型名,而非关键字class或typename来指定非类型参数。 当一个模板被实例化时,非类型模板参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。

 template <unsigned N,unsigned M> int compare(const char (&p1)[N] ,const char (&p2)[M]) //这里是数组的引用,引用形参绑定到对应的实参上,即绑定到数组上。 {     return strcmp(p1,p2); }

如上代码中,编写了一个处理字符串字面常量的模板函数。这种字面常量是const char的数组。由于不能拷贝一个数组,因此我将函数参数定义为数组的引用。模板定义为了两个非类型的参数,从而我们可以比较不同长度的字符串字面常量。第一个参数表示第一个数组的长度,第二个参数表示第二个数组的长度。

compare("hello","C++");因此在调用此语句时,编译器会用字面常量的大学来代替N和M,从而实例化模板。编译器会在一个字符串字面常量的末尾插入一个空字符来作为终结符。因此编译器会实例化为 int compare(const char (&p1)[6] ,const char (&p2)[4])

一个非类型参数可以是个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整数参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须是具有静态的生存期。不能使用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以用nullptr或一个值为0的常量表达式来实例化。

在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。

 // 定义一个模板类型的静态数组  template<class T, size_t N = 10>  class array  {  public:      T& operator[](size_t index){return _array[index];}      const T& operator[](size_t index)const{return _array[index];} ​      size_t size()const{return _size;}      bool empty()const{return 0 == _size;}    private:      T _array[N];      size_t _size;  };

浮点数、类对象以及字符串是不允许作为非类型模板参数的。

非类型的模板参数必须在编译期就能确认结果。

4、模板的编译

当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用(而不是定义)模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。 通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。 模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。(函数模板和类模板成员函数的定义通常放在头文件中,模板不能声明和定义分离在两个文件中。)

使用模板的注意事项!!!

当使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的提供者来保证的。而且,模板的提供者必须保证,当模板被实例化时,模板的定义,包括类模板的成员的定义,也必须是可见的。 用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须是可见的,这是由模板的用户来保证的。


二、模板的特化

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。

  struct Stu  {      char name[40];      double grades;      int age;  }

由于C++允许将一个结构赋给另一个结构,假如交换两个Stu结构的内容。使用上述Swap模板函数,依旧可行。

然而,假识想交换 grades 和 age 成员,而不交换 name 成员,则需要使用不同的代码,但 Swap()的参数将保持不支(两个Stu结构的引用),因此无法使用模板重载来提供其他的代码。 然而,可以提供一个具体化函数定义称为显式具体化(explicit specialization),其中包含所需的代码,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

具体化机制随着C++的演变而不断变化。下面介绍C++标准定义的形式。

对于给定的函数名,可以有非模板函数、模板函数、显示具体化模板函数以及它们的重载的版本。

显示具体化的原型和定义应以 template<>打头,并通过名称来指出类型。

具体化优先于常规模板,而非模板函数优先于具体化和常规模板。

下面是交换Stu结构体的几个函数的声明:

 //非模板函数 void SWap(Stu &a,Stu &b); //模板函数 template<typename T> void Swap(T &,T &) //显式具体化 template <> void Swap<Stu>(Stu&,Stu&);

Swap<Stu>中的<Stu>是可选的,因为函数的参数类型表名,这是Stu的一个具体化。因此,该定义也可以这样写 template <> void Swap(Stu&,Stu&);

 //在函数名后的<>中指定模板参数的实际类型 //情况1 template<typename T> void Swap(T a, T b) {     T t = a;     a = b;     b = t; }  ​ //情况2 template<typename T> void Swap(T& a, T& b) {     T t = a;     a = b;     b = t; } ​ int main() {     int a = 1;     double  b = 1.1;     Swap<double>(b, a);//情况1可以使用,情况2不可以使用     cout << a << " " << b << endl; }

情况2中将类型double生成一个显示实例化。由于第一个参数的类型是double&,不能指向int变量a。

在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。


1、函数模板特化

函数模板的特化步骤:

必须要先有一个基础的函数模板 。

关键字template后面接一对空的尖括号<> 。

函数名后跟一对尖括号,尖括号中指定需要特化的类型。

函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

 // 函数模板 -- 参数匹配 template<class T> bool greater(T left, T right) {  return left > right; } // 对Less函数模板进行特化 template<> bool greater<Date*>(Date* left, Date* right) {      return *left > *right; } int main() {      cout << greater(1, 2) << endl;      Date d1(2022, 7, 7);      Date d2(2022, 7, 8);      cout << greater(d1, d2) << endl;      Date* p1 = &d1;      Date* p2 = &d2;      cout << greater(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成      return 0; }

2、类模板特化

a.全特化

全特化是指将模板参数列表中所有参数都确定化。

 template<class T1, class T2> class Data { public:     Data() {cout<<"Data<T1, T2>" <<endl;} private:      T1 _d1;      T2 _d2; }; template<> class Data<int, char> { public:     Data() {cout<<"Data<int, char>" <<endl;} private:      int _d1;      char _d2 }; void TestVector() {  Data<int, int> d1;  Data<int, char> d2; } 

b.偏特化

偏特化是指任何针对模版参数进一步进行条件限制设计的特化版本。

 template<class T1, class T2> class Data { public:  Data() {cout<<"Data<T1, T2>" <<endl;} private:  T1 _d1;  T2 _d2; };

偏特化由两者表现方式:部分特化和限制参数的特化

部分特化:将参数类表中的一部分参数特化。

 template <class T1>// 将第二个参数特化为int class Data<T1, int> { public:      Data() {cout<<"Data<T1, int>" <<endl;} private:     T1 _d1;     int _d2; }; 

限制参数的特化

 //两个参数偏特化为指针类型 template <typename T1, typename T2> class Data <T1*, T2*> {  public:     Data() {cout<<"Data<T1*, T2*>" <<endl;}   private:      T1 _d1;      T2 _d2; }; //两个参数偏特化为引用类型 template <typename T1, typename T2> class Data <T1&, T2&> { public:     Data(const T1& d1, const T2& d2)      : _d1(d1)      , _d2(d2) {     cout<<"Data<T1&, T2&>" <<endl; }   private:      const T1 & _d1;      const T2 & _d2;   }; void test2 ()  {      Data<double , int> d1; // 调用特化的int版本      Data<int , double> d2; // 调用基础的模板       Data<int *, int*> d3; // 调用特化的指针版本      Data<int&, int&> d4(1, 2); // 调用特化的指针版本 }

匹配顺序:全特化 -> 偏特化 -> 原模板


三、模板相关定义

类模板(class template) :模板定义,可从它实例化出特定的类。类模板的定义以关键宇 template 开始,后跟尖括号对<和>,其内为一个用逗号分隔的一个或多个模板参数的列表,随后是类的定义。

默认模板实参(default template argument):一个类型或一个值,当用户未提供对应模板实参时,模板会使用它。

显式实例化(explicit instantiation):一个声明,为所有模板参数提供了显式实参。

实例 (instantiation) :编译器从模板生成的类或函数。

函数模板(function template): 模板定义,可从它实例化出特定函数。函数模板的定义以关键宇 template 开始,后跟尖括号对<和>,其内为一个用逗号分隔的一个或多个模板参数的列表,随后是函数的定义。

成员模板(member template): 本身是模板的成员函数。成员模板不能是虚函数。


点击全文阅读


本文链接:http://zhangshiyu.com/post/68232.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1