C++:模板进阶

文章目录


前言

前面我们学到了模板的基本使用方法,可以做模板函数,也可以做模板类,像这样:

cpp 复制代码
template<class T>
void Printf()
{
    T a = 10;
    cout << a << endl;
}

也可以像这样写成类模板

cpp 复制代码
template<class T>
class A
{
public:
private:
    A _a;
}

那么今天我们来学习模板进阶的知识点与使用~( *^-^)ρ(*╯^╰)


一、typename的作用

1.1 问题引入

假设说我现在有这样的一个需求,就是需要打印一个vector类型的 v 中的数据。

cpp 复制代码
int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

这样就用范围for打印了数据,我们看到可以正常打印。


那如果我想写一个函数模拟这个行为呢?

我们可以用一个迭代器来模拟这个行为,

cpp 复制代码
void Print(const vector<int>& v)
{
	vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

1.2 问题分析

那如果是想打印list或者其他容器呢?

我们怎么样来写一个模板?

很多同学可能认为需要这样写:

cpp 复制代码
template<class Container>
void Print(Container con)
{
	Container::const_iterator it = con.begin();
	while (it != con.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

但是问题就出现了,我们在编译的时候会看见这个报错信息:

这是为什么呢?

这是因为对于编译器来说,这句话有两层含义:

编译器在从上到下编译的过程当中遇到了红框中的这句话,这句话可以是一个静态成员变量,也可以是一个类型,因此编译器不确定这到底是什么,所以会报错。

对于静态成员变量可以这样理解:

cpp 复制代码
class A
{
public:
	int begin()
	{
		return 1;
	}
private:
	//类内声明
	static int const_iterator;
};
//类外定义
int A::const_iterator = 0;

我可以有一个静态成员变量叫做const_iterator,那我们在别的类里面使用的时候是不是应该写成A::const_iterator,和Container::const_iterator是一样的。

Container::const_iterator还有可能它本身是一个类型,也是我们所期盼的样子,你是原生指针你就是原生指针,你是别的就是别的,因此这里编译会出问题。

为什么这里有问题前面没有呢?

因为Container::const_iterator并没有实例化,编译器不知道这是什么,而前面这个版本已经实例化过了vector<int>::const_iterator it = v.begin();


1.3 问题处理

解决方式也很简单,就是在Container::const_iterator前加一个typename,表示这是一个类型,编译器你不用随意猜测啦!

cpp 复制代码
template<class Container>
void Print(Container con)
{
	typename Container::const_iterator it = con.begin();
	while (it != con.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

这样就可以正常打印,这里以vector和list做例子,结果是这样的:

对于编译器来说,加了typename就是明确告诉编译器这里是类型,等模板实例化再去找。

这里还有一种简单的方法,就是使用auto来接受,auto不会产生歧义,它本身就是一种类型。

cpp 复制代码
template<class Container>
void Print(Container con)
{
	auto it = con.begin();
	while (it != con.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

但是auto也不是万能的,在这种情况下就不能使用auto了:

这里因为没有接受的值,他是一个模板参数,因此这里只能用typename


二、非类型模板参数

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

类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

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

非类型模板参数主要用于下面这个场景:

假设说我想定义一个静态栈,我应该怎么写呢?

cpp 复制代码
#define N 100

// 静态栈
template<class T>
class Stack
{
private:
	T _a[N];
	int _top;
};

int main()
{
	Stack<int> st1; // 10
	Stack<int> st2; // 100

	return 0;
}

这里面就会出现一个问题,假如我st1想要10个空间大小,st2想要100个空间大小,那么这个N开小了就不够,开大了就会造成浪费。

要怎么解决呢?

我们可以这样:

cpp 复制代码
// 静态栈
template<class T, size_t N>
class Stack
{
private:
	T _a[N];
	int _top;
};

int main()
{
	Stack<int, 10> st1; // 10
	Stack<int, 100> st2; // 100

	return 0;
}


定义的时候多加一个参数,这个N是一个常量,是不可以修改的,这样传什么就开多少空间。

注意1:N是一个常量,是不可以进行修改的。

注意2:这里面除了size_t还可以传其他整形家族的值,如 int 和 char,但是对于比如double,string就不可以了!

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


三、array容器

array就是数组,不过就是套了个壳子,这个和int[]是一模一样的,没有任何区别,array唯一与数组区别就是array对越界的检验非常严格,越界读写都能检查。普通数组,不能检查越界读,少部分越界写可以检查。

但是用array检查还不如用vector呢,vector还能初始化~~~


四、模板的特化

4.1 函数模板的特化

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

如:我们要写一个比较大小的模板类

cpp 复制代码
template<class T>
bool Less(T left, T right)
{
	return left < right;
}

但是我现在想比较指针应该怎么办呢?这样子写比较的是地址。

我们就可以用函数模板的特化,语法如下:

  1. 不写tmplate<>里的东西
  2. 函数名后面加要写的类型如<int*>
  3. 参数就用这个类型
  4. 写特化之前一定要存在原来最开始的模板
cpp 复制代码
template<>
bool Less<int*>(int* left, int* right)
{
	return *left < *right;
}

但是对于函数模板的刻画,建议直接写成类似函数重载的形式,比较简单,如下:

cpp 复制代码
bool Less(int* left, int* right)
{
	return *left < *right;
}

这样有现成的编译器就会吃现成的,对于其他类型就会走模板实例化出来一个具体的函数。


4.2 类模板特化

假设有一个Data类:

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

我想对它的参数为(int, double)时进行特殊处理,应该进行类模板全特化

语法如下:

  1. 不写tmplate<>里的东西
  2. 函数名后面加要写的类型如<int, double>这样两个给全了就叫做全特化
  3. 写特化之前一定要存在原来最开始的模板
  4. 不写成员变量了,写也可以,不过要与之前的类型保持一致,因此不如不写
cpp 复制代码
//全特化
template<>
class Data<int, double>
{
public:
	Data() { cout << "Data<int, double>" << endl; }
private:
};

假设我只想匹配第二个参数也可以用半特化:

cpp 复制代码
//半特化
template<class T1>
class Data<T1, double>
{
public:
	Data() { cout << "Data<T1, double>" << endl; }
private:
};

甚至可以进一步半特化,对于两个参数都特化,也可以传指针,也可以传引用:

cpp 复制代码
//进一步半特化
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
private:
};

//进一步半特化
template<class T1, class T2>
class Data<T1&, T2&>
{
public:
	Data() { cout << "Data<T1&, T2&>" << endl; }
private:
};

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。

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


五、模板分离编译

5.1 为什么要进行模板分离编译?

什么是分离编译

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

那为什么我们在写list,vector等的时候没有进行声明和定义分离呢?

因为模板定义的分离就会出现很多问题,假设有如下场景:

假设.h文件中对于push以及pop分离了定义与声明,还有一个fun1函数分离声明定义,fun2函数只声明不定义。

cpp 复制代码
#pragma once
#include<deque>

namespace jyf
{
	template<class T, class Container = std::deque<T>>
	class stack
	{
	public:
		void push(const T& x);
		void pop();

		T& top()
		{
			return _con.back();
		}

		size_t size()
		{
			return _con.size();
		}

		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
	};

	class A
	{
	public:
		void func1(int i);
		void func2();
	};
}
cpp 复制代码
#include"Stack.h"
namespace jyf
{
	template<class T, class Container>
	void stack<T, Container>::push(const T& x)
	{
		_con.push_back(x);
	}

	template<class T, class Container>
	void stack<T, Container>::pop()
	{
		_con.pop_back();
	}

	void A::func1(int i)
	{}

	//void func2();
}

此时进行编译就会出现大问题:

cpp 复制代码
#include"Stack.h"

int main()
{
	jyf::stack<int> st;  // call xxstackxx(0x324242)
	st.push(1);  // call xxpushi(?)
	st.pop();

	st.size();   // call xxsizexx(0xdadada)
	st.top();

	jyf::A aa;
	aa.func1(1);   // call xxfunc1xx(?)
	//aa.func2();  // call xxfunc2xx(?)

	jyf::stack<double> st1;  // call xxstackxx(0x324242)
	st1.push(1);  // call xxpushi(?)
	st1.pop();

	return 0;
}

为什么会出现这样的问题呢?

对于fun2可以理解,因为fun2没有定义,在链接的时候就找不到他的地址,无法call他的地址,只有定义了才有地址!

但是为什么对于模板不可以呢?

我们先来看编译链接的过程:

原因:

编译时实例化: 模板的实例化是在编译过程中根据具体类型生成的。因此,编译器必须在看到模板的定义时才能够生成对应类型的代码。如果模板的定义放在实现文件(如 .cpp 文件)中,而声明在头文件中,当其他文件包含这个头文件时,编译器并不会找到模板的定义,因此无法实例化模板。

模板实例化需要定义: 当模板函数或类在某个翻译单元(translation unit)中被使用时,编译器必须能够在同一个翻译单元中找到模板的完整定义。如果模板定义和声明分离,其他文件引用该模板时编译器找不到定义,编译就会失败。

链接期问题: 普通函数在编译时生成代码,编译器只需要知道它的声明,而定义可以在别的文件中,链接器会在链接阶段将它们结合起来。但是,模板的实例化发生在编译期,而不是链接期。由于模板在编译期生成代码,因此模板的定义必须在编译时可见,否则编译器就无法生成相应的实例化代码。

也就是说,声明就是告诉编译器我写了,你放心去编译吧,但是在连接的是=时候,并没有实例化函数,也就是根本没有他的地址,就会出错!


5.2 解决方式

第一种解决方式就是对于模板,将声明和定义放在一起。

第二种解决方式时,在.cpp文件下告诉编译器我的实例化:

cpp 复制代码
#include"Stack.h"
namespace jyf
{
	template<class T, class Container>
	void stack<T, Container>::push(const T& x)
	{
		_con.push_back(x);
	}

	template<class T, class Container>
	void stack<T, Container>::pop()
	{
		_con.pop_back();
	}

	void A::func1(int i)
	{}

	//void func2();

	template
		class stack<int>;

	template
		class stack<double>;
}

但是这种方法不好,一次只能实例化一个,我们想要double还要额外写。

库里面对于这里,对于短的函数就直接写,对于长的函数,在.h文件中进行声明与定义分离,这样就解决了问题。


总结

优点:

  1. 代码复用

    模板通过泛型编程,使得相同的代码能够处理不同的数据类型,大幅减少重复代码,从而节省开发资源。正因为模板机制,C++标准模板库(STL)得以实现,为容器、迭代器、算法等提供了通用、高效的解决方案。

  2. 增强代码灵活性

    模板使得代码更加通用和灵活,允许用户定义的类型作为参数传递,扩展了函数和类的适用性。例如,使用模板可以定义一个不依赖于特定数据类型的栈类(如 stack<int>stack<float> 等),大大提高了代码的可扩展性。

缺陷:

  1. 代码膨胀(Code Bloat)

    模板在编译时会根据不同的类型实例化出不同的代码,这可能导致二进制文件体积变大。对于大量使用模板的程序,每一种模板参数类型都会生成一份独立的代码,造成一定程度的代码冗余和膨胀问题。

  2. 编译时间长

    模板的实例化通常发生在编译时,编译器需要为每个使用模板的不同类型生成相应的代码,这会显著增加编译时间。尤其是使用复杂模板嵌套时,编译过程可能变得很慢。

  3. 编译错误信息复杂

    模板引发的编译错误往往难以阅读和定位。由于模板在实例化过程中生成的代码是由模板定义和使用的类型决定的,编译器有时会输出大量的模板展开细节,错误信息非常冗长,给调试和维护带来困难。


到这里就结束啦,谢谢大家!!!💕💕💕😍😍😍(❤️´艸`❤️)

相关推荐
Ddddddd_1583 小时前
C++ | Leetcode C++题解之第504题七进制数
c++·leetcode·题解
J_z_Yang3 小时前
LeetCode 202 - 快乐数
c++·算法·leetcode
Y.O.U..6 小时前
STL学习-容器适配器
开发语言·c++·学习·stl·1024程序员节
lihao lihao6 小时前
C++stack和queue的模拟实现
开发语言·c++
姆路7 小时前
QT中使用图表之QChart概述
c++·qt
西几7 小时前
代码训练营 day48|LeetCode 300,LeetCode 674,LeetCode 718
c++·算法·leetcode
风清扬_jd7 小时前
Chromium HTML5 新的 Input 类型week对应c++
前端·c++·html5
南东山人8 小时前
C++静态成员变量需要在类外进行定义和初始化-error LNK2001:无法解析的外部符号
c++
lqqjuly8 小时前
C++ 中回调函数的实现方式-函数指针
开发语言·c++
2401_871120358 小时前
数组与指针基础
c++