C++之模板进阶

Posted 小赵小赵福星高照~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++之模板进阶相关的知识,希望对你有一定的参考价值。

模板进阶


没了解过模板的读者,先学习模板初阶: C++之模板初阶

通过模板我们可以实现泛型编程,模板分为函数模板和类模板,下面我们就说点模板进阶的一些东西。

非类型模板参数

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

举个例子,比如我们写个静态栈结构:

#define N 10
template<class T>//类型模板参数
class Stack

private:
    T _a[N];
    size_t _top;
;
int main()

    Stack<int> st1;//大小为10
    Stack<int> st2;
    

我们想要改栈的大小就改宏就可以了,但是我们有两个栈呢?一个栈的大小想要10,另一个栈想要1000的大小,这样就不能满足多个栈的需求了,除非再定义一个类模板,但是这样代价太大,在C++模板当中有一个非类型的模板参数概念:

template<class T,size_t N>
    //T是类型模板参数,N是非类型模板参数,N是一个常量
class Stack

private:
    T _a[N];
    size_t _top;
;
int main()

    Stack<int,100> st1;//100
    Stack<int,20000> st2;//20000

我们这样就可以通过传参完成每个栈想要的大小需求了,在template<class T,size_t N>当中,T是类型模板参数,这里的N是非类型模板参数,这里的N是一个常量

我们呢可以这样传参吗?

int main()

    static int n;
    cin>>n;
    Stack<int,n> st;//error,非类型模板参数不能是变量
    return 0;

这样是错误的,非类型模板参数不能是变量

在STL中的容器当中,C++11新增了array这个容器,array这个容器就是类似这样的结构,它使用了非类型的模板参数:

template<class T,size_t N>
class Array

private:
    T _a[N];

array是一个大小固定的容器

但是array这个容器不建议使用,为什么呢?

函数调用会建立栈帧,数组过大,可能会造成栈溢出,用vector的话,空间不够就增容,比较灵活,增容是在堆区开辟空间,而堆区时进行动态开辟的地方,它的空间较大,知道需要的数据大小直接使用vector中的resize就好了,没必要使用array这个容器,这里可以知道C++11增加的array容器基本没有什么用,它的缺点大于它的优点。

C++缺点之一:后期C++11等等标准增加了不少鸡肋的语法,让语言变得臃肿,学习成本增加,一些刚需的东西,姗姗来迟,甚至还没来(网络库)。

非类型模板参数缺省值

模板参数都可以给缺省值,模板参数给缺省值和函数参数给缺省值是完全类似的,可以全缺省,也可以半缺省(必须从右往左连续缺省)

比如:

//模板参数都可以给缺省值
//模板参数给缺省值和函数参数给缺省值是完全类似的
//可以全缺省
//也可以半缺省 -- 必须从右往左连续缺省
template<class T,size_t N = 10>
class Array

private:
    T _a[N];

int main()

    Array<int> a1;
    Array<int,20> a2;
    return 0;

需要注意的是,如果全都是缺省值时不能这样创建对象:

Array a1;

全部都是缺省值,我们可以不传参数,但是我们知道Array是个模板,模板也是有类型的,我们需要这样:

Array<> a1;

注意

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
template<class T,string s1>
template<class T,double s1>

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

模板的特化

概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,比如:

template<class T>
bool IsEqual(const T& left,const T& right)
  
    return left==right; 

int main()

    cout<<IsEqual(1,2)<<endl;
    
    char p1[] = "hello";
    char p2[] = "hello";
    cout<<IsEqual(p1,p2)<<endl;//数组名是指针常量
    
    return 0;

这个模板,用来比较整形可以使用,但是我们用来比较字符串呢?这样就出问题了。

我们想一想可能可以这样解决:

template<class T>
bool IsEqual(const T& left,const T& right)

    if(T == const char*)
    
    	return strcmp(left,right)==0;    
        //可是语法不支持
    
    else
    
    	return left==right; 
    

这样貌似也可以实现字符串的比较,但是这个是语法不支持的,所以不能这样。

模板的特化,针对某些类型进行特殊化处理,我们可以这样写:

bool IsEqual(const char*& left,const char*& right)

    return strcmp(left,right)==0;

int main()

    cout<<IsEqual(1,2)<<endl;
    
    char p1[] = "hello";
    char p2[] = "hello";
    cout<<IsEqual(p1,p2)<<endl;//数组名是指针常量
    
    return 0;

我们调式过后,发现这里不会进这个函数,因为数组名是指针常量,则这里的const修饰的是*left,是left指向的内容不能修改,而不是left不能修改,这里属于权限放大了

需要这样改,这样就可以进去了:

//模板的特化,针对某些类型进行特殊化处理
bool IsEqual(const char*& const left,const char*& const right)

    return strcmp(left,right)==0;

也可以这样改,将引用去掉:

bool IsEqual(const char* left,const char* right)

    return strcmp(left,right)==0;

函数模板的特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<class T>
void Swap(T& a,T& b)

    //vector代价太大
    T tmp = a;
    a = b;
    b = tmp;

int main()

    int x = 1;
    int y = 2;
    Swap(x,y);
    vector<int> v1 = 1,2,3,4;
    vector<int> v2 = 10,20,30,40;
    Swap(v1,v2);
    return 0;

当我们交换的类型为vector时,此时用模板函数进行交换代价太大了,一次拷贝构造+两次赋值重载,所以我们可以这样写:

//函数模板的特化
template<>
void Swap<vector<int>>(vector<int>& a,vector<int>& b)

    a.swap(b);

这就是函数模板的特化,有点类似于指定类型进行显式实例化

当然也可以这样,利用模板的匹配原则,进行特殊化处理:

//模板的匹配原则,进行特殊化处理
void Swap(vector<int>& a,vector<int>& b)

    a.swap(b);

类模板的特化

全特化

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

template<class T1,class T2>
class Data

public:
	Data()  cout << "Data<T1,T2>"<<endl; 
private:
	T1 _d1;
	T2 _d2;
;
//全特化
template<>
class Data<double, double>

public:
	Data()  cout << "Data<double,double>" << endl; 
private:
	T1 _d1;
	T2 _d2;
;

int main()

    Data<int,int> d1;
    Data<double,double> d2;
    return 0;

偏特化

偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:

偏特化有两种表现方式:

  • 部分特化,将模板参数类表中的一部分参数特化。
template<class T1,class T2>
class Data

public:
	Data()  cout << "Data<T1,T2>"<<endl; 
private:
	T1 _d1;
	T2 _d2;
;
//偏特化或者半特化
template<class T1>
class Data<T1,char>

public:
	Data()  cout << "Data<T1,double>" << endl; 
;

int main()

    Data<double,char> d2;
    Data<int,char> d1;
    
    return 0;

  • 参数更进一步的限制,偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
template<class T1,class T2>
class Data

public:
	Data()  cout << "Data<T1,T2>"<<endl; 
private:
	T1 _d1;
	T2 _d2;
;
//偏特化或者半特化:不一定是特化部分参数,有可能是对参数的限制
template<class T1,class T2>
class Data<T1*,T2*>

public:
	Data()  cout << "Data<T1*,T2*>" << endl; 
;

template<class T1,class T2>
class Data<T1&,T2&>

public:
	Data()  cout << "Data<T1*,T2*>" << endl; 
;
int main()

    Data<int*,char*> d5;
    Data<int*,int*> d6;
    
    Data<int&,char&> d7;
    Data<int&,int&> d8;
    return 0;

模板分离编译

什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

首先上结论:模板不支持分离编译

我们正常写模板是需要声明和定义放在一起的,是因为模板不支持分离编译:

//.h文件
template<class T>
void F(const T& x)

    cout<<"void F(const T& x)"<<endl;

下面我们来验证不支持分离编译的原因是什么:

首先在.h文件中写模板的声明:

template<class T>
void F(const T& x);//声明

在.cpp中写模板的定义:

#include"Func.h"
template<class T>
void F(const T& x)//定义

    cout << "void F(const T& x)" << endl;

在test.cpp中测试:

#include"Func.h"
int main()

    F(1);

此时出现了链接错误,为什么我们平时使用的普通函数不会报错,而模板函数会报链接错误的呢?

首先我们有这三个文件:

Func.h Func.cpp test.cpp

程序生成可执行程序的过程是编译和链接,编译阶段又分为预处理、编译、汇编三个阶段:

1、预处理

预处理阶段进行头文件展开、宏替换、条件编译、去注释

预处理之后生成的文件是Func.i、test.i,Func.cpp和test.cpp分别变成了:

Func.i

template<class T>
void F(const T& x);
void F(const T& x)

    cout<<"void F(const T& x)"<<endl;

Test.i

template<class T>
void F(const T& x);
int main()

    F(1);

2、编译

编译阶段进行语法检查,语义分析,符号汇总等等,最后生成汇编代码

对应生成的文件是Func.s和test.s

3、汇编

汇编阶段是把汇编代码转成二进制机器码,并且生成符号表(定义在本文件中的函数有明确的地址,没有定义在本文件中的函数,符号表中还没有地址)

对应生成的文件是Func.o和test.o

4、链接

在链接阶段把类型test.o里面F和Print这样没有函数地址的地方,拿名字去其他目标文件中去找,找到以后填到符号表。再把目标文件合并到一起,生成可执行程序

模板的实例化是在编译阶段要做的事情,在编译阶段,Func.i生成Func.s时并不知道T是什么类型,实例化的指令test.i文件里才知道,所以并没有实例化,而在链接之前它们不进行交汇,各自干各自的事情,Func.i生成Func.s没有实例化生成,所以在链接时候不会找到F函数模板生成的实例化函数,就发生了链接错误。

解决方案一

在Func.cpp文件中显式指定实例化

template
void F(const int& x);

缺陷:用一个类型就得显式实例化一个,非常麻烦

解决方案二

不分离编译。声明和定义或者直接定义在.h中

对于类也是一样的:

Func.h

#include<iostream>
using namespace std;
template<class T>
void F(const T& x)//定义

    cout << "void F(const T& x)" << endl;

template<class T>
class Stack

public:
    Stack();
    ~Stack();
private:
    T* _a;
    int _top;
    int _capacity;
;

Func.cpp

#include"Func.h"
template<class T>
Stack<T>::Stack()

	_a = new T[10];
	_top = 0;
	_capacity = 10;

template<class T>
Stack<T>::~Stack()

	delete[] _a;
	_a = nullptr;

test.cpp

#include"Func.h"
int main()

    Stack<int> st;

此时运行程序也会发生链接错误:

我们的解决方法和函数模板是完全类似的:

要么在Func.cpp文件中显式指定实例化,要么不进行分离编译

对于类模板,还有一个概念:按需实例化

比如我们还另外写了push函数

#include<iostream>
using namespace std;
template<class T>
void F(const T& x)//定义

    cout << "void F(const T& x)" << endl;

template<class T>
class Stack

public:
	Stack()
	
		_a = new T[10];
		_top = 0;
		_capacity = 10;
	
	template<class T>
	~Stack()
	
		delete[] _a;
		_a = nullptr;
	
	void push(const T& x)
	
		_a[_top] = x;
		_top++;
	
private:
    T* _a;
    int _top;
    int _capacity;
;

但是我们在test.cpp当中不使用栈的push操作:

#include"Func.h"
int main()

    Stack<int> st;
    return 0;

此时我们故意将push函数弄出个语法错误,比如去掉分号:

void push(const T& x)

	_a[_top] = x
	_top++;

push有语法问题,没有检测出来,编译没有报错

原因

模板如果没有实例化,编译器不会去检查模板内部语法错误,我们实例化了栈这个类,对类模板是按需实例化,调用了哪个成员函数就实例化谁

当我们使用push成员函数时:

此时就会报错了。

模板总结

优点

  • 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
  • 增强了代码的灵活性

缺点

  • 模板会导致代码膨胀问题,也会导致编译时间变长
  • 出现模板编译错误时,错误信息非常凌乱,不易定位错误

总的来说模板的优点是远大于缺点的

以上是关于C++之模板进阶的主要内容,如果未能解决你的问题,请参考以下文章

C++入门篇(15)之模板知识进阶

C++入门篇(15)之模板知识进阶

C++模板进阶

C++模板进阶

[ C++ ] template 模板进阶 (特化,分离编译)

C++ 树进阶系列之树状数组的树形之路