『C++ - 模板』之模板进阶

简介: 『C++ - 模板』之模板进阶

模板进阶

非类型模板参数

类型模板参数与非类型模板参数的不同

  • 类型模板参数
  • 非类型模板参数

类型模板参数一般用来设置模板的类型;

而非类型模板参数默认为整形常量;

同时作为模板参数它们都可以进行定义缺省值;(同时,由于是整型常量,所以只能作为模板参数而不能再其他地方再进行赋值,因为左值不能被修改,而N为整形常量为左值)

#include<iostream>
using namespace std;
template<class T = int,int N = 20>//T为类型模板参数,N为非类型模板参数
//一般来说 类型模板参数后面跟的都是类型,而一般非类型模板参数后面一般为整型常量
//(包括int/char/short/long/long long等)
    /* bool 类型也可以作为非类型模板参数的类型,bool也属于整型家族 */
class Array{
  T _a[N];
};
void test_1(){
  Array<int,10> _a1;
  Array<double,20> _a2;
}
int main()
{
  test_1();
  return 0;
}


模板的特化

模板的特化分为两种,分为类模板的特化以及函数模板的特化(函数模板暂不支持偏特化)

同时还可以分为全特化偏特化(部分特化);


全特化

特化一般至在原有泛型编程的基础上,对某些类型或者参数进行特殊的处理;

假设有一个仿函数的函数模板,该函数模板可以对同个类型进行较小比较;

template<class T>
struct Less{
  bool operator()(const T&a,const T&b){
    return a<b;
  }
};
void test_2(){
  int a = 10,b = 20;
  Less<int> lessfunc;
  cout<<lessfunc(a,b)<<endl;
}

运行test_2函数后所得的结果为1;

但是若是传的数据类型为指针类型,则讲出现不一样的答案;

void test_2(){
  int a = 10,b = 20;
  Less<int*> lessfunc1;
  cout<<lessfunc1(&a,&b)<<endl;
}

由于在在C++中指针也可以进行比较,所以在这里也进行了比较,但是打印出的结果为0;

原因是虽然进行了比较但是比较只是指针中的单纯比较,并不是我们需要的结果;

而在C++中却出现了模板特化的语法;

//原模版
template<class T>
struct Less{
  bool operator()(const T&a,const T&b){
    return a<b;
  }
};
//模板的特化
template<>//语法中模板特化不需要定义模板参数
struct Less<int*>{//而在此处时应该声明需要特化的类型
  bool operator()(const int *a,const int *b){//根据特定的类型对其特化需要的操作
    return *a<*b;
  }
};
void test_2(){
  int a = 10,b = 20;
  Less<int> lessfunc;
 //cout<<lessfunc(a,b)<<endl;
  Less<int*> lessfunc1;
  cout<<lessfunc1(&a,&b)<<endl;
}

运行上面这段程序将可以得出预期的结果1;

在模板的特化中需要注意特化的语法;

同时在模板的特化时需要注意:

  • 在进行模板特化时必须存在一个原有的模板;
    模板的特化是在原有的模板中进行一种类似重载的操作;

同时模板的特化和函数的重载有一定的区别,在一个类中,相应的类与其模板可以同时存在;

且相应的函数模板也可以与其相应的函数声明同时存在;

只不过一个属于模板,一个属于声明;(模板只有在调用的时候根据模板参数的类型去实例化相应的类/函数)

上段代码演示的为类模板特化;

template<class T>
  bool operator()(const T&a,const T&b){
    return a<b;
  }
template<>
  bool operator()(const int *a,const int *b){//根据特定的类型对其特化需要的操作
    return *a<*b;
  }

↑函数模板特化↑


在大多数情况下,函数模板特化的使用较少;

因为在大多情况下函数可以进行重载(模板与声明可以同时存在,构成重载);

同时应该注意,函数模板暂不支持偏特化;


偏特化

对于模板的特化除了全特化以外还有偏特化;

全特化类似于重载,但是对于偏特化而言,只是在原有的模板基础上增添(进一步进行)了类型的限制;

即可以对一定类型的模板参数进行特殊处理;

//原模板
template<class T>
struct Less{
  bool operator()(const T&a,const T&b){
    return a<b;
  }
};
//偏特化处理
template<class T>//对于全特化而言,<>内不需要模板参数,而偏特化时需要显示原模板参数
struct Less<T*>{
  bool operator()(const T*a,const T*b)const{
    return *a<*b;
  }
};
void test_3(){
  int a = 10;
  int b = 20;
  cout<<Less<int>()(a,b)<<endl;
  cout<<Less<int*>()(&a,&b)<<endl;
}

该处对test_3函数进行调用时得出的结果都未true;

与全特化处理而言,偏特化处理更适合处理某一类型的特殊类型,在这里演示了统一对指针进行处理;


全特化与偏特化的区别

全特化与偏特化的命名方式与特化的模板参数数量无关;

只与特化的模板参数的规则有关;

全特化与偏特化的区别;

这个规则是指是否完全限制所有模板参数以致达到全特化的效果;

如:

template<class T1,class T2>
class print{
  void Print(T1 a,T2 b){
      cout<<"T1,T2"<<endl;
      cout<<"原模板"<<endl;
  }
};
template<class T1>
class print<T1>{
  void Print(T1 a,int* b){
      cout<<"T1,int*"<<endl;
      cout<<"偏特化"<<endl;
  }
};
template<class T1,class T2>
class print<T1,T2>{
  void Print(T1 a,T2* b){
    cout<<"T1,T2*"<<endl;
      cout<<"偏特化"<<endl;
  }
}
template<class T1>
class print<T1>{
    void Print(T1* a,char* b){
      cout<<"T1*,char*"<<endl;
        cout<<"偏特化"<<endl;
  }
};

如上段代码而言,这里的特化行为并没有指定所有的模板参数为某个单独的类型,而是限制了部分模板参数使其为某一类的类型或者说是某一种单独类型;

所以在这里可以将偏特化分为三种表现形式:

  1. 部分模板参数指定类型进行特化( 例: < T1 , T2 > 特化为 < T1 , int* > );
  2. 所有模板参数将其特化为该类型的限制( 例: < T1 , T2 > 特化为 < T1* , T2& > );
  3. 以上两种的集合( 例: < T1 , T2 > 特化为 < T1* , double* > )


模板的分离编译

对于模板来说是不支持分离编译的,即分文件进行声明与定义;

但是在同文件种是可以进行声明定义分离的;

为什么在多文件的情况下不支持声明与定义分离?

首先我们要了解c/C++程序的翻译过程;

一个程序的翻译过程一般包括四步:

  • 预处理在预处理阶段中,一般会进行以下操作:
  1. 头文件展开
  2. 宏替换
  3. 条件编译
  4. 去注释等
  • 同时在该阶段,原来的.c\.cpp被翻译过后将会生成一个.i后缀文件;
    同时在该次翻译过后,代码一样也为c/C++;
  • 编译
    该该阶段中,.i文件将会被翻译成.s后缀的汇编代码;
  • 汇编
    在该阶段中将会把.s后缀的文件翻译成.o的可重定向二进制目标文件;
  • 链接
    在该过程中,将会把多个.o可重定向二进制目标文件进行链接,最后生成.exe可执行程序;

假设有三个文件Func.h文件用于声明,Func.cpp用于定义,main.cpp用于测试;

三个文件内的代码分别为:

  • Func.h
#pragma once
template<class T>
void Add(const T& a, const T& b);//函数模板声明
void func(int a,int b);//函数声明
  • Func.cpp
#include<iostream>
#include"Func.h"
using namespace std;
template<class T>
void Add(const T& a, const T& b){
    cout<<a+b<<endl;
}
void func(int a,int b){
    cout<<a+b<<endl;
}
  • main.cpp
#include<iostream>
#include"Func.h"
using namespace std;
int main()
{
    func(10,20);
    Add(10,20);
    Add(5.5,2,2);
    return 0;
}

假设运行上面这段程序将会出现链接失败;

连接失败的原因即为模板进行了分离编译;

从该图可以看出程序翻译的过程;

其实在最初的预处理中的展开头文件就能了解到问题的所在;

首先模板是一个未经过实例化的存在;

在这个程序中,func()函数可以被直接进行调用的原因是因为即使声明定义分离,函数的定义也仍然存在主体;

但是对于模板来说是一个不存在实体即没有被实例化的存在;

在首先的预处理阶段,头文件展开,此时main.cpp文件内与Func.cpp文件内的内容大概如下;

/*
*main.cpp
*/
#include<iostream>//在该演示中不便于展开该头文件
#pragma once
template<class T>
void Add(const T& a, const T& b);//函数模板声明
void func(int a,int b);//函数声明
using namespace std;
int main()
{
    func(10,20);
    Add(10,20);
    Add(5.5,2,2);
    return 0;
}
/*
*Func.cpp
*/
#include<iostream>//在该演示中不便于展开该头文件
#pragma once
template<class T>
void Add(const T& a, const T& b);//函数模板声明
void func(int a,int b);//函数声明
using namespace std;
template<class T>
void Add(const T& a, const T& b){
    cout<<a+b<<endl;
}
void func(int a,int b){
    cout<<a+b<<endl;
}

从上面两个文件可以看出,为什么在编译过程中并没有报错而最后在链接的过程中才进行报错;

在编译过程中,main.i文件内由于头文件的展开,已经存在了模板;

意思当Add()函数调用时,找到了对应的声明;

同理func()函数也如此;

而函数真正的调用在链接阶段,链接阶段将会通过对应声明的函数的地址找到函数的定义从而进行调用;

但是在这里实际中Add()函数模板并未进行实例化,所以才导致的链接错误;

解决办法

那有什么办法可以解决对于模板的分离编译吗?

  • 对模板进行显式实例化
    当在模板定义的文件中进行模板的显式实例化即可解决一部分问题;
    当然只是一部分的问题;
#include<iostream>
#include"Func.h"
using namespace std;
template<class T>
void Add(const T& a, const T& b){
    cout<<a+b<<endl;
}
template
void Add(const int& a ,const int& b);
template
void Add<double>(const double& a ,const double& b);
//以上两种皆可
void func(int a,int b){
    cout<<a+b<<endl;
}
  • 由于进行了显式实例化,在调用的过程中会根据变量的类型优先匹配该文件内的对应的声明
    template void Add<double>(const double& a ,const double& b);
    根据该声明实例化一份对应类型的函数;

  • 分离声明定义时不采用分离编译
    即不将声明与定义分离问两个文件;

总结

模板的优缺点

  • 优点
  1. 模板采用了泛型编程,可以进行代码的复用从而可以高效的进行开发;
  2. 模板增强了代码的灵活性;
  • 缺点
  1. 模板的使用将会造成代码膨胀;
    即在原来的编译过程中只需要进行语法的判断,而对于模板而言则多增加了一个步骤为模板的实例化;
    不同的类型实例化出的代码也不同从而导致代码膨胀,同时也导致了编译时间变长;
  2. 当使用模板进行开发时,若是出现模板的编译错误,则大量的错误信息使在开发过程中不能精确找到错误位置;
相关文章
|
3月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
126 10
|
2月前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
64 4
|
2月前
|
算法 编译器 C++
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
37 3
|
2月前
|
编译器 C++
【c++】模板详解(1)
本文介绍了C++中的模板概念,包括函数模板和类模板,强调了模板作为泛型编程基础的重要性。函数模板允许创建类型无关的函数,类模板则能根据不同的类型生成不同的类。文章通过具体示例详细解释了模板的定义、实例化及匹配原则,帮助读者理解模板机制,为学习STL打下基础。
34 0
|
3月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
24 1
|
3月前
|
存储 编译器 C++
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
55 9
|
3月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
74 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
3月前
|
算法 编译器 C++
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
99 2
|
3月前
|
存储 算法 编译器
【C++】初识C++模板与STL
【C++】初识C++模板与STL
|
3月前
|
编译器 C++
【C++】模板进阶:深入解析模板特化
【C++】模板进阶:深入解析模板特化
120 0