C++模板初阶(重要)

目录

泛型编程

函数模板

函数模板的实例化

模板参数的匹配原则

类模板

显示实例化

编译器的查找过程

显式实例化类模板

显式实例化函数模板 

优缺点


泛型编程

产生原因:当我们试图实现一个通用的交换函数,我们会重载多个接收不同参数类型的swap函数,但这样会使得代码复用率低,只要有新类型出现,就需要增加新的重载函数

基本概念:泛型编程一种编程范式,它允许编写可以处理不同类型数据的代码,而无需针对每种数据类型都重复编写相同的逻辑,C++实现泛型编程的核心是模板

模板的分类:类模板、函数模板

void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

void Swap(double& left, double& right)
{
    double temp = left;
    left = right;
    right = temp;
}

void Swap(char& left, char& right)
{
    char temp = left;
    left = right;
    right = temp;
}

......

函数模板

基本概念:表示了同一类功能的函数,在使用时该模板会根据实参类型产生函数的特定类型版本

格式:

template <typename T1,typename T2......typename Tn>
返回值类型 函数名(参数列表){函数体}

template <class T1,class T2......class Tn>
返回值类型 函数名(参数列表){函数体}
#include <iostream>
using namespace std;

template<typename T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}

int main()
{
    int a = 1, b = 2;
    Swap(a, b);
    cout << a <<"   "<< b << endl;
    double d1 = 1.1, d2 = 2.2;
    Swap(d1, d2);
    cout << d1 <<" "<< d2 << endl;
    return 0;
}

注意事项:

1、typenameclass 都可以用于定义模板参数T,它们在模板声明中常常是等价的,但也有一些细微的区别,使用嵌套类型时,必须使用 typename,因为 class 不能在这种上下文中使用

template <typename T>
void func(typename T::value_type val) 
{
    // 使用 T 的嵌套类型 value_type
}
  • T::value_type 是依赖于模板参数 T 的类型。为了告诉编译器 value_type 是一个类型而不是其他东西,需要使用 typename 

2、函数模板是一个蓝图,不是函数,模板就是会将本来应该程序员要做的重复的事情交给编译器 

函数模板的实例化

基本概念:用不同类型的参数使用函数模板时,模板函数会被实例化出不同的函数

分类:隐式实例化(编译器根据实参自行推演实例化出的函数形参类型,但可能会出错)、显式实例化(在函数名后使用<>指定模板参数的实际类型)

#include <iostream>
using namespace std;

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, a2);//正确
	Add(d1, d2);//正确

	Add(a1, d1);//错误
	return 0;
}

运行结果:编译错误,编译器在推演第三个要实例化出的Add函数时,实参a1将T推演为int,实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者double类型而报错

问题:为什么编译器不能将其中一个类型转化为另一个呢?这样不是就不会出错了吗?

解释:在涉及模板的操作中,编译器一般不会进行类型转换,因为一旦转化出问题,编译器就需要背黑锅,为了避免隐式实例化编译器无法正确推断类型的问题,需要用户进行显式实例化或者提前将进行类型的强制转换

//用户自行强转类型
Add((double)a1, d1); 
Add(a1, (int)d1); 

//显式实例化
Add<int>(a, b);
Add<double>(a,b);

注意事项:模板函数不允许自动类型转换,因为编译器要求模板参数的类型必须精确匹配,以确保类型安全性,而普通函数可以进行自动类型转换,为了提升代码的灵活性

模板参数的匹配原则

1、同名的非模板函数和函数模板可以同时存在

#include <iostream>
using namespace std;

template<typename T>
T Add(const T& left, const T& right)
{
    return left + right;
}

int Add(const int& left, const int& right)
{
    return left + right;
}

int main()
{
    int a = 1, b = 2;
    double d1 = 1.1, d2 = 2.2;
    Add(a, b);
    Add(d1, d2);
    return 0;
}

2、如果其它条件相同,优先调用非模板函数而不是模板函数,如果模板可以产生一个具有更好匹配的函数,那么就选择模板

#include <iostream>
using namespace std;

template<typename T>
T Add(const T& left, const T& right)
{
    return left + right;
}

int Add(const int& left, const int& right)
{
    return left + right;
}

int main()
{
    int a = 1, b = 2;
    double d1 = 1.1, d2 = 2.2;
    Add(a, b);//调用自定义的普通函数
    Add(d1, d2);//调用函数模板
    return 0;
}

类模板

格式:

template<class/typename T1,class/typename T2>//typename和class均能定义模板类型
class 类模板名
{
    //类内成员定义
}
#include <iostream>
using namespace std;

template <class T>
class Stack
{
public:
    void Push(const T& x)
    {}
private:
    T* _a;
    int _top;
    int _capacity;
};
int main()
{
    //同一类模板实例化出的两个类型
    Stack<int> st1;//栈中的数据类型实例化成int
    Stack<double> st2;//栈中的数据类型实例化成double
    return 0;
}

注意事项:

1、类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<T的类型>

2、 类模板中的函数放在类外进行定义时,需要加模板参数列表,但一般不会这样干

template<class T>
class Vector
{
public:
	Vector(size_t capacity = 10)
		: _pData(new T[capacity])
		, _size(0)
		, _capacity(capacity)
	{}

	~Vector();

	void PushBack(const T& data);
	void PopBack();
	size_t Size() { return _size; }

	T& operator[](size_t pos)
	{
		assert(pos < _size);
		return _pData[pos];
	}

private:
	T* _pData;
	size_t _size;
	size_t _capacity;
};

template <class T>
Vector<T>::~Vector()
{
	if (_pData)
		delete[] _pData;
	_size = _capacity = 0;
}

3、类模板的声明和定义不建议声明和定义分离因为未实例化的模板都是一个逻辑符号没有实际代码,同时需要注意的是类实例化发生在编译阶段,假设将A类的声明放在A.h、定义放在A.cpp、然后在B.cpp中要实力化一个A类对象(两个cpp文件均包含了头文件但并未互相包含),在编译阶段遇到B.cpp中的实例化请求时会去寻找A类的定义,但此时是编译阶段不是链接阶段,所以不能看到A.cpp中A类的定义,最终报错

显示实例化

基本概念:显式实例化是指,在某些特定情况下,程序员手动告诉编译器要生成特定类型的模板实例从而避免不必要的模板实例生成(在某个 .cpp 文件中明确指定生成某些类型的模板实例),减少模板的冗余编译(也是实现类模板的声明和定义分离的一种方式)

声明和定义的语法(函数模板类似):

//显式实例化定义的语法
template class ClassName<T>;

//显式实例化声明的语法
extern template class ClassName<T>;
  • 指示编译器生成 ClassName<T> 的模板实例,一般用于 .cpp 文件,表示模板的特定类型实例应在此文件中生成

编译器的查找过程

基本概念:在理解模板声明和定义分离的查找过程之前,我们需要先了解模板的实例化过程分为 声明(只有各种成员的名字)、使用(要使用模板实例化对象的请求)定义(模板中成员的具体实现) 的三个主要阶段

承接上面分析模板定义和声明分离的例子:A.h 中声明类模板 A,在 A.cpp 中为特定类型(例如 A<int>)提供显式实例化定义。当编译器在编译 B.cpp 时,遇到对 A<int> 的实例化请求,它会根据 A.h 中的声明和 extern template class A<int>;(显式实例化声明),知道 A<int> 的代码已经在 A.cpp 中生成。因此,编译器不会尝试再次实例化 A<int>,而是等待链接阶段使用 A.cpp 中已经生成的实例化代码。这样,模板的定义可以安全地与声明分离,而不会在编译时出错

显式实例化类模板

// MyTemplate.h
#ifndef MYTEMPLATE_H
#define MYTEMPLATE_H//条件编译,遇到#include "MyTemplate.h"才会展开下面的内容

#include <iostream>

template<typename T>
class MyTemplate {
public:
    MyTemplate(T value);
    void display() const;
private:
    T data;
};

// 显式实例化声明
extern template class MyTemplate<int>;

#endif // MYTEMPLATE_H
// MyTemplate.cpp
#include "MyTemplate.h"

template<typename T>
MyTemplate<T>::MyTemplate(T value) : data(value) {}

template<typename T>
void MyTemplate<T>::display() const {
    std::cout << data << std::endl;
}

// 显式实例化定义
template class MyTemplate<int>;
  • MyTemplate.h:包含模板的声明和会进行外部模板实例化声明 extern template class MyTemplate<int>表示 MyTemplate<int> 的实例化将在其他地方完成
  • MyTemplate.cpp包含模板的定义和 MyTemplate<int> 的显式实例化定义 template class MyTemplate<int>指示编译器在这个 .cpp 文件中生成 MyTemplate<int> 的代码
  • 这样,当你在其他地方使用 MyTemplate<int> 时,不会再次实例化 int 类型,编译器只会使用 MyTemplate.cpp 中生成的实例化代码

显式实例化函数模板 

// MyFunction.h
template<typename T>
void printValue(T value);

// 声明显式实例化声明
extern template void printValue<int>(int);

// MyFunction.cpp
#include "MyFunction.h"
#include <iostream>

template<typename T>
void printValue(T value) {
    std::cout << value << std::endl;
}

// 显式实例化定义
template void printValue<int>(int);

优缺点

优点:

  1. 减少重复实例化,提升编译速度:通过显式实例化,你可以避免多个文件对同一个模板进行重复实例化,从而加快编译速度。
  2. 隐藏实现细节:你可以将模板的实现放在 .cpp 文件中,而头文件中只保留接口声明,避免暴露实现细节。
  3. 控制代码大小:只为特定类型实例化模板,可以减少二进制文件大小,避免不必要的代码膨胀。
  4. 提高灵活性:你可以精确控制哪些类型可以实例化模板,限制不必要的类型使用。

缺点:

  1. 灵活性降低:显式实例化意味着你需要提前决定哪些类型可以使用模板,如果后来需要支持新的类型,必须重新修改代码。
  2. 维护难度增加:在大型项目中,显式实例化可能增加维护的复杂性,因为你需要跟踪每个模板的实例化情况。
  3. 不适合广泛使用的模板:如果模板是一个通用的库组件,显式实例化可能过于繁琐,不如直接放在头文件中来得方便。

补充:显示实例化只是支持类模板声明和定义分离的方式之一,Pimpl 技术、C++20的模块化编程等都可以实现这一操作 

~over~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值