C++模板进阶

本文介绍了C++中类型参数和非类型模板参数的区别,着重讨论了类模板的特化,包括全特化和偏特化,以及模板分离编译的原理、问题和解决方案。同时,提到了模板的优缺点,如代码复用、灵活性和可能带来的代码膨胀和错误定位困难。
摘要由CSDN通过智能技术生成

1.非类型模板参数

首先,类型参数分为类型形参非类型形参

类型形参:在模板参数中,在class或者typename之后的参数类型名称

非类型模板参数:就是用一个常量作为类(函数)的一个参数,在类(函数)模板中可以将该参数当成常量来使用

#pragma once
#include<iostream>
using namespace std;


namespace bear
{
    //定义一个模板类型的静态数组
    template<class T,size_t N = 10> //类型形参 T,非类型形参 N
    class array
    {
    public:
        size_t arraysize()
        {
            return N;
        }

    private:
        T _array[N];//使用非类型形参N可以当成一个常量来创建数组
    };
}

 在代码完成之后,就可以尝试创建想要的数组了

void test()
    {
        array<int, 10> a1;//定义一个int类型,大小为10的数组
        cout << a1.arraysize() << endl;//10

        array<int, 100> a2;//定义一个int类型,大小为100的数组
        cout << a2.arraysize() << endl;//100

    }

 注意

1.浮点数,类对象以及字符串是不可以作为非类型模板参数的。

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

2.类模板的特化

概念

通常情况下,我们使用模板可以实现一些与类型无关的代码,但是在一些特殊类型的情况下可能会出错,需要特殊处理

先看一个函数模板:

template<class T>
bool Less(T left, T right)//比较两个数字大小
{
    return left < right;
}

 一般我们会进行这样的比较:

int main()
{
    cout << Less(1, 2) << endl;//1,1比2小,输出正确
    return 0;
}

 那么在复杂的情况下:

template<class T>
bool Less(T left, T right)
{
 return left < right;
}
int main()
{
 cout << Less(1, 2) << endl; // 可以比较,结果正确
 Date d1(2022, 7, 7);
 Date d2(2022, 7, 8);
 cout << Less(d1, d2) << endl; // 可以比较,结果正确
 Date* p1 = &d1;
 Date* p2 = &d2;
 cout << Less(p1, p2) << endl; // 可以比较,结果错误
 return 0;
}

 可以看到,Less在大多数情况下都可以正常比较吗,但是在特殊场景下就得到错误的结果,例如上述代码,p1指向的d1显然小于p2指向的d2对象,Less内部只是对指针的地址进行比较,从而导致错误

函数模板的特化

但是,在遇到指针类型的时候就会出错,此时并不是对值进行比较,而是对指针的地址进行比较,这时候就不是我们想要的结果了,我们想要的是解引用后进行比较。这时候就是我们所说的出错的情况

所以,这时候就需要引出 类模板的特化 这一概念

我们先来看看如何特化:

template<class T>
bool Less(T left, T right)
{
    return left < right;
}

template<>
bool Less<int*>(int* left, int* right)//对int指针的数据进行比较
{
    return *left < *right;//解引用比较
}

 看完代码就可以总结特化的步骤了

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

 除了int*还有什么场景呢?可以看到只要是参数是指针类型的基本都可以用特化,例如Date类型,char*类型等,会自动走模板特化后的代码。

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将函数直接给出

也就是

//正常情况下
template<>
bool Less<int*>(int* left, int* right)//对int指针的数据进行比较
{
    return *left < *right;//解引用比较
}

//特殊情况下
template<>
bool Less(int* left, int* right)
{
 return *left < *right;
}

类模板的特化

全特化

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

 例如,对于下列类模板

template<class T1, class T2>
class bear
{
public:
	bear()
	{
		cout << "bear<T1, T2>" << endl;
	}
private:
	T1 _D1;
	T2 _D2;
};

 当T1和T2分别为int和double,我们想对实例化的类进行特殊处理,那么就可以对该情况下的类模板进行全特化

//对于T1是double,T2是int时进行特化
template<>
class bear<double, int>
{
public:
	bear()
	{
		cout << "bear<double, int>" << endl;
	}
private:
	double _D1;
	int _D2;
};

 那么如何知道此时走的就是特化之后的模板呢?

 利用构造函数进行打印,就可以在控制台上查看实际走的是哪个了

偏特化

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

 例如,对于下列类模板

template<class T1, class T2>
class bear
{
public:
	bear()
	{
		cout << "bear<T1, T2>" << endl;
	}
private:
	T1 _D1;
	T2 _D2;
};

 偏特化有两种方式表现

1.部分特化

也就是将模板参数类表中的一部分参数特化

template<class T1, class T2>
class bear
{
public:
	bear()
	{
		cout << "bear<T1, T2>" << endl;
	}
private:
	T1 _D1;
	T2 _D2;
};


template<class T2>
class bear<int, T2>
{
public:
	bear()
	{
		cout << "bear<int, T2>" << endl;
	}
private:
	int _D1;
	T2 _D2;
};

 也就是说,只有当T1为int类型是才会走特化模板。

2.参数更进一步的限制

偏特化不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本

我们可以将两个参数(T1和T2)特化为指针类型,也可以是引用类型

//两个参数偏特化为指针类型
template<class T1, class T2>
class bear<T1*, T2*>
{
public:
	bear()
	{
		cout << "Dragon<T1*, T2*>" << endl;
	}
private:
	T1 _D1;
	T2 _D2;
};

//两个参数偏特化为引用类型
template<class T1, class T2>
class bear<T1&, T2&>
{
public:
	bear()
	{
		cout << "bear<T1&, T2&>" << endl;
	}
private:
	T1 _D1;
	T2 _D2;
};

 这时候也可以继续利用构造函数来打印以便确定走的到底是哪个模板

3.模板的分离编译

什么是分离编译

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

 例如,在之前的STL容器模拟实现的时候,大家就很喜欢在.h头文件中写代码,如何在.cpp源文件中直接测试即可

模板的分离编译

在分离编译模式下,我们一般创建三个文件,一个头文件用于进行函数声明,一个源文件用于对头文件中声明的函数进行定义,最后一个源文件用于调用头文件当中的函数。


按照此方法,我们若是对一个加法函数模板进行分离编译,其三个文件当中的内容大致如下:

 

但是使用这三个文件生成可执行文件时,却会在链接阶段产生报错。

 

下面我们对其进行分析:
我们都知道,程序要运行起来一般要经历以下四个步骤:

1.预处理: 头文件展开、去注释、宏替换、条件编译等。
2.编译: 检查代码的规范性、是否有语法错误等,确定代码实际要做的工作,在检查无误后,将代码翻译成汇编语言。
3.汇编: 把编译阶段生成的文件转成目标文件。
4.链接: 将生成的各个目标文件进行链接,生成可执行文件。


以上代码在预处理阶段需要进行头文件的包含以及去注释操作。

 

 这三个文件经过预处理后实际上就只有两个文件了,若是对应到Linux操作系统当中,此时就生成了 Add.i 和 main.i 文件了。

预处理后就需要进行编译,虽然在 main.i 当中有调用Add函数的代码,但是在 main.i 里面也有Add函数模板的声明,因此在编译阶段并不会发现任何语法错误,之后便顺利将 Add.i 和 main.i 翻译成了汇编语言,对应到Linux操作系统当中就生成了 Add.s 和 main.s 文件。

之后就到达了汇编阶段,此阶段利用 Add.s 和 main.s 这两个文件分别生成了两个目标文件,对应到Linux操作系统当中就是生成了 Add.o 和 main.o 两个目标文件。

前面的预处理、编译和汇编都没有问题,现在就需要将生成的两个目标文件进行链接操作了,但在链接时发现,在main函数当中调用的两个Add函数实际上并没有被真正定义,主要原因是函数模板并没有生成对应的函数,因为在全过程中都没有实例化过函数模板的模板参数T,所以函数模板根本就不知道该实例化T为何类型的函数。

模板分离编译失败的原因:
在函数模板定义的地方(Add.cpp)没有进行实例化,而在需要实例化函数的地方(main.cpp)没有模板函数的定义,无法进行实例化。

如何解决

解决类似于上述模板分离编译失败的方法有两个,第一个就是在模板定义的位置进行显示实例化。
例如,对于上述代码解决方案如下:

 

在函数模板定义的地方,对T为int和double类型的函数进行了显示实例化,这样在链接时就不会找不到对应函数的定义了,也就能正确执行代码了。

虽然第一种方法能够解决模板分离编译失败的问题,但是我们这里并不推荐这种方法,因为我们需要用到一个函数模板实例化的函数,就需要自己手动显示实例化一个函数,非常麻烦。

现在就来说说解决该问题的第二个方法,也是我们所推荐的,那就是对于模板来说最好不要进行分离编译,不论是函数模板还是类模板,将模板的声明和定义都放到一个文件当中就行了。

 综上所述:

1.将声明和定义放到同一个.h头文件中,这是最推荐的

2.模板定义的位置显示实例化。这是不推荐的,也不实用

4.模板总结

优点

1.模板服用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生

2.增强了代码的灵活性

缺点

1.模板会导致代码膨胀问题,也会导致编译时间变长

2.出现模板编译错误时,错误信息非常凌乱,不易定位错误

  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值