“编写一次,无限应用:深入理解C++模板“

在这里插入图片描述

write in front
所属专栏: C++学习
️博客主页:睿睿的博客主页
️代码仓库:VS2022_C语言仓库
您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我你们将会看到更多的优质内容!!


文章目录

  • 前言
  • 一.静态对象和类型的混淆:
  • 二.模板的特化:
    • 1.函数模板特化:
    • 2.类模板特化:
      • 全特化:
      • 偏特化:
        • a.特化部分参数:
        • b.参数的进一步修饰:
      • 具体使用场景:
  • 三.模板分离编译
    • 3.1 什么是分离编译
    • 3.2 模板的分离编译
    • 3.3 解决方法
  • 总结模板的优缺点:

前言

  在了解完模板初阶以后,我们来学习进阶的知识!

一.静态对象和类型的混淆:

  我们先来看看这个代码:

template<class Container>
void print(const Container &v)
{
	Container::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}

void test1()
{
vector<int> vv;
print(vv);
}

  这段代码在运行的时候会出错,可是为什么呢?

我们在类里面用::访问的时候有两种情况:

  1. 静态成员对象
  2. 类型

  编译器不知道这里的访问是静态对象还是类型名,所以这里就会报错。

那么怎么解决呢?

  我们只要在类型名之前加一个typename或者直接用auto来表示类型就可以了。

  所以,在类模板或者函数模板中,当你引用模板参数的嵌套类型时,最好在前面加上 typename,这样可以消除歧义,使代码更具可读性,也有助于编译器正确地解析代码。

二.模板的特化:

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

// 函数模板 -- 参数匹配
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;
}

  如果我们通过指针比大小,那肯定是错误的。此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化类模板特化

1.函数模板特化:

  我们先来看看一个比较大小的函数

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}

  如果此时我们通过指针想要比较他们的大小时,就可在这个函数的基础上特化出一个新模板:

template<>
bool Less<int*>(int* x, int * y)
{
	return *x < *y;
}

  但是对于函数模板的特化,我们可以直接重新写一个,就没必要在特化一个新的了。
  当然,我们也可以通过限制类型来特化一个模板函数:

template<class T>
bool Less(T* x, T* y)
{
	return *x < *y;
}

重点

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

2.类模板特化:

  类模板特化分为两种:全特化和偏特化

全特化:

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

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

  通过运行发现,如果有符合的全特化的类,那么就不用在通过模板实例化一份类出来。

偏特化:

  偏特化也有两种情况:

a.特化部分参数:

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

  这样的偏特化,就将第二个参数定死为double类型,只要第二个类型为double,而且没有更具体的类,我们就会调用这个类。

b.参数的进一步修饰:

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

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

  在类后面对参数进行限制,第一个偏特化限制为指针,第二个偏特化限制为引用。

具体使用场景:

  我们通过算法库里的sort函数排序来调用仿函数的例子来使用一下类模板的特化:

#include
#include 
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 6);
Date d3(2022, 7, 8);
vector<Date> v1;
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 可以直接排序,结果是日期升序
sort(v1.begin(), v1.end(), Less<Date>());
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序
// 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
// 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
sort(v2.begin(), v2.end(), Less<Date*>());
return 0;
}

  通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指针,结果就不一定正确。因为:sort最终按照Less模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题

// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};

特化之后,在运行上述代码,就可以得到正确的结果

三.模板分离编译

3.1 什么是分离编译

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

3.2 模板的分离编译

假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

// a.h
template<class T>
T Add(const T& left, const T& right);
void func1(int a,int b)void func2(double a,double b)// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
void func1(int a,int b)
{
return a+b;
}
void func2(double a,double b)
{
return a+b;
}
// main.cpp
#include"a.h"
int main()
{
func1(1,2);
func2(1.0,2.0);
Add("abc","def");
return 0;
}

  在分析前我们先来回顾一下编译链接的相关知识:
“编写一次,无限应用:深入理解C++模板“_第1张图片
“编写一次,无限应用:深入理解C++模板“_第2张图片

在编译运行时我们会发现:

  • func1链接查到了
  • func2链接查找不到,因为只有声明没有定义
  • Add链接查找不到,但是我们定义了呀

  那么为什么会出现这样的情况呢?其实啊,我们发现func1可以编译成功,是因为他是一段“具体”的函数,而我们知道,模板实例化是在编译的时候进行的,Add函数在编译的时候只是说可以在头文件找到他的声明,但是并没有对这个模板实例化。所以在链接的时候我们不能找到实例化的函数地址与之对应,链接就会报错。

3.3 解决方法

  1. 将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h其实也是可以的。推荐使用这种。
  2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用:
    比如在刚刚的a.cpp文件下加一个:
template
Add<string>;

总结模板的优缺点:

【优点】

  1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
  2. 增强了代码的灵活性(适配器和仿函数的使用)

【缺陷】

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

  更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

专栏订阅:
每日一题
C语言学习
算法
智力题
初阶数据结构
Linux学习
C++学习
更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

你可能感兴趣的:(c++,c++,java,开发语言,模板方法模式,学习)