【C++】模板进阶

模板

1. 非类型模板参数

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

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

非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。模板参数不只是类型,还可以是编译期常量。

cpp 复制代码
////非类型模板参数
//namespace cl
//{
//	// 定义一个模板类型的静态数组
//	template<class T, size_t N>
//	class array
//	{
//	public:
//		T& operator[](size_t index)
//		{
//			return _array[index];
//		}
//
//		const T& operator[](size_t index) const
//		{
//			return _array[index];
//		}
//
//		size_t size() const
//		{
//			return _size;
//		}
//
//		bool empty() const
//		{
//			return _size == 0;
//		}
//	private:
//		T _array[N];
//		size_t _size;
//		
//	};
//}

//#define N =100	
//template<class T>
//class stack
//{
//private:
//	T _a[N];
//	int _top;
//	int _capacity;
//};
//
////当我想把s1的n赋值为10,s2的n赋值为100无法在一个模板当中解决
//
//int  main()
//{
//	stack<int> s1;
//	stack<int> s2;
//	return 0;
//}


//`size_t N`:非类型模板参数 NTTP,必须是编译期常量
template<class T,size_t N =100>
class stack 
{
private:
	T _a[N];
	int _top;
	int _capacity;
};


int main()
{
	stack<int,10> s1;//10
	stack<int, 100> s2;//100
	stack<int> s3;//100
	stack<int> s4;//100

	//int x;
	//cin >> x;
	////原因:非类型模板参数`N`必须是编译期常量。
	////变量`x`的值是运行时输入,编译器在编译阶段无法知道 x 的值,不能拿来作为模板参数。
	//stack<int, x> s5;
	cout << sizeof(s1) << endl;
	cout << sizeof(s2) << endl;

}

// //C++20支持
//template<class T, double X>
//class B
//{};

注意:

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

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

2. 模板的特化

2.1 概念

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

cpp 复制代码
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
//1. 函数模板本身不会报错; 只有当你用 Date 类型去调用 Less 的时候,模板才会实例化,才会触发编译错误 。
//2. 模板对类型有要求:传入的类型 T, 必须支持模板内部用到的所有操作(这里就是必须支持`<`)。
//3. 内置类型(int / double)自带`<`;自定义类默认没有`<`,必须手动重载。
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;
	//`T` 被推导成 `Date`(Date 指针),调用的是指针原生的`<`,不是 Date 对象的 operator<。
	//指针的`<`是什么?

	//	C++ 内置:指针之间可以直接用`< > <= >=`,比较的是内存地址大小
	//	`p1 < p2`:比较 `d1` 的内存地址 和 `d2` 的内存地址谁大。
	//	根本不会去解引用,不会对比两个 Date 里面的年月日!
	cout << Less(p1, p2) << endl; // 可以比较,结果错误
	//d1、d2 是两个独立的局部变量,它们在栈上的位置由编译器分配:
	//	- 有可能 d1 地址更小 → p1 < p2 为真
	//	- 也有可能 d1 地址更大 → p1 < p2 为假
	//	和日期大小没有任何关系
	//	这就是题目说:能编译通过,但是结果错误的根源。
	return 0;
}

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示 例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内 容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。 此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方 式。模板特化中分为函数模板特化与类模板特化。

2.2 函数模板特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板

  2. 关键字template后面接一对空的尖括号<>

  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型

  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇 怪的错误。

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

// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

注意:参数类型要匹配

cpp 复制代码
Less(1, 2.2);
// 第一个实参int → T=int
// 第二个实参double → T=double
// 推导矛盾!编译器不知道T选int还是double,直接报错

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

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

该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化 时特别给出,因此函数模板不建议特化。

2.3 类模板特化

2.3.1 全特化

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

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

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

int main()
{
	TestVector();
	return 0;
}

2.3.2 偏特化

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

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

偏特化有以下两种表现方式:

部分特化

将模板参数类表中的一部分参数特化。将第二个参数特化为int

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

参数更进一步的限制

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

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

};

template<typename T1, typename T2>
class Data<T1&, T2&>
{
public:
	// 构造函数增加初始化列表,初始化引用成员引用成员
	//引用作为类的成员变量,有一条铁则:引用必须在【初始化列表】初始化,不能留空!
	Data(const T1& d1, const T2& d2)
		: _d1(d1)
		, _d2(d2)
	{
		cout << "Data<T1&, T2&>" << endl;
	}
private:
	const T1& _d1;
	const T2& _d2;//引用成员

};
void test2()
{
	Data<double, int> d1;// 调用特化的int版本
	
	Data<int, double> d2;// 调用基础的模板    
	Data<int*, int*> d3;// 调用特化的指针版本
	Data<int&, int&> d4(1,2);  // 调用特化的引用版本
}


int main()
{
	test2();
	return 0;
}

3 模板分离编译

3.1 什么是分离编译

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

3.2 模板的分离编译

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

1. 普通类的分离编译(对比,方便理解)

普通类可以分离:头文件放声明,cpp 放实现

cpp 复制代码
// Test.h
class Test
{
public:
	void Func();
};

// Test.cpp
#include "Test.h"
void Test::Func()
{
}

编译:Test.cpp单独编译成目标文件Test.obj;main.cpp 编译成 main.obj,最后链接。 普通类成员函数是实体,编译阶段就生成机器码。

2. 模板为什么不行?

模板不是一个真实的类 / 函数,只是一段代码蓝图。

模板只有实例化的时候,编译器才会生成对应类型的函数 / 类实体。

cpp 复制代码
// template.h
template<class T>
void Func(T x);  // 只有声明,实现放在template.cpp

// template.cpp
#include "template.h"
template<class T>
void Func(T x)
{
	cout << x << endl;
}

// main.cpp
#include "template.h"
int main()
{
	Func(10); // 需要实例化 Func<int>
	return 0;
}

编译过程:

  1. 编译 template.cpp:没有任何地方实例化模板 ,编译器不会生成Func<int>的机器码,只把模板源码留在 obj 里。
  2. 编译 main.cpp:看到Func(10),只看到函数声明,编译器假设这个函数实体在别的 obj,编译成功。
  3. 链接阶段 :找Func<int>的实体,找不到 → 链接错误 undefined reference to void Func<int>(int)

3. 解决模板分离编译的 3 种方案

方案 1:模板声明和实现全部写在头文件(最常用,工程首选)

.hpp 专门放模板代码,#include 到需要的地方。

cpp 复制代码
// template.hpp
#pragma once
#include<iostream>
template<class T>
void Func(T x)
{
	cout << x << endl;
}

main 里直接#include "template.hpp"

原理:include 相当于把模板源码直接拷贝进 main.cpp,实例化的时候编译器能看到完整实现。

方案 2:显式实例化

在模板的 cpp 文件末尾,手动指定要实例化的类型,告诉编译器预先生成这些类型的实体。

cpp 复制代码
// template.h
template<class T>
void Func(T x);

// template.cpp
#include "template.h"
template<class T>
void Func(T x)
{
	cout << x << endl;
}
// 显式实例化,强制生成Func<int>、Func<double>
template void Func<int>(int);
template void Func<double>(double);

缺点:需要预先知道所有会用到的类型,新增类型就要手动加一行实例化,不灵活。适合类型固定的场景。

方案 3:把模板实现文件 #include 到头文件末尾(不推荐,但考题会提)

cpp 复制代码
// template.h
template<class T>
void Func(T x);

#include "template.cpp"  // 放在头文件末尾

本质和方案 1 一样,只是把实现拆分到单独 cpp,只是语法上分开,编译时还是全部展开。

相关推荐
卢锡荣1 小时前
乐得瑞Type-c接口产品描述
c语言·开发语言
Shadow(⊙o⊙)1 小时前
仿muduo实现高并发服务器——时间轮timewheel.cpp
c语言·c++
木井巳1 小时前
【动态规划】三步问题
java·算法·leetcode·动态规划·推荐算法
楚识科技1 小时前
手写 OCR 识别技术深度解析:从算法原理到政企级落地实现
算法·ocr
leobertlan10 小时前
痛苦系列 | DSP-03 跨越单位圆:Z变换与零极点分析
算法·程序员
蛋蛋的就会顺顺的10 小时前
hot100 p0——栈,贪心算法,动态规划
java·前端·javascript·算法·leetcode·力扣
开发小能手12311 小时前
答案daanC语言
数据结构·算法
死磕音视频11 小时前
让小模型做选择题:限定候选项并读取 logits 概率的入门实践(附 Python 代码)
开发语言·python·大模型·文本分类·transformers
Navigator_Z12 小时前
LeetCode //C - 1291. Sequential Digits
c语言·算法·leetcode