您现在的位置是:首页 >其他 >【C++】—— 模板介绍网站首页其他

【C++】—— 模板介绍

起飞的风筝 2024-09-15 00:01:03
简介【C++】—— 模板介绍

前言:

在之前的学习中,我们已经对几个常见的STL库容器进行了详细的讲解,并且进行了模拟实现帮助大家立即。接下来,我们要介绍的就是关于 “模板” 的基本知识。


目录

前言

(一)非类型模板参数

1、基本介绍

(二)类模板的特化

1、定义

2、使用场景

 3、 函数模板特化

4、类模板特化

1️⃣ 全特化

2️⃣ 偏特化

5、类模板特化应用示例

(三)模板的分离编译

1、什么是模板分离编译?

2、 模板的分离编译

3、 解决方法 

总结


前言

首先我们要了解什么是模板。简单来说,模板提供了一种通用的编程方式,可以用于实现泛型编程,从而使得程序更加灵活和可复用。它可以让我们编写可以适用于多种数据类型的函数和类。使用模板可以避免重复编写相似的代码,并且能够提高代码的复用性、可读性和可维护性。

在C++模板中,通常使用类模板和函数模板两种方式来实现泛型编程。在之前的文章中,我已经对泛型编程 ,函数模板 以及类模板 进行了相应的介绍,今天我们将学习的是 1. 非类型模板参数 2. 类模板的特化 3. 模板的分离编译 这三个方面的知识。如果大家对于之前的还有不理解的,大家可以参考我这篇文章 :模板初阶介绍

C++中的模板是一种非常强大的编程工具,可以帮助开发者更高效地编写通用代码。下面就来详细介绍一下C++中的模板。


(一)非类型模板参数

首先模板参数分为类型形参非类型形参

  1. 类型形参:简单点来说定义的就是类型,出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
  2. 非类型形参:与类型参数对应的就是关于非类型参数。就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

那么什么时候用这个非类型参数呢?接下来利用代码为大家进行介绍:

#define N 20

template<class T>
class Arry
{
public:

private:
	T _a[N];
};

int main()
{
	Arry<int>aa1;   
	Arry<double>aa2;


	return 0;
}

解释

  1. 对于上述代码我们简单的实现一个静态的数组,依照之前的思维,我们可能就会像上述这样,定眼一看觉得没什么大问题,当我们想要 【int】的数据时就给出相应的int,其他的同理;
  2. 但是呢此时就出现一个问题,上述我们定义的 aa1 想要 10个数据,二 aa2 想要100个数据时,上述这样的写法还能满足对应的需求吗?答案很明显,此时 define 这样的一个宏就不满足条件了;
  3. 不知道大家是否感到十分熟悉这一块,就跟我们之前的 【typedef】一样,关于【typedef】的问题我们解决不了,因此就引入了模板参数(例如一个存int,一个存double)。

?  因此为了解决上述这样的问题,我们就引入了非类型模板参数来解决这个问题!!!

1、基本介绍

  1. C++中的非类型模板参数允许用户在编译时提供参数来实例化模板。
  2. 非类型模板参数是在编译时确定的,不像类型模板参数那样在运行时确定,因此非类型模板参数具有一些和普通函数参数不同的特性。
  3. 一般而言,非类型模板参数都被定义为常量表达式(const expression)。常量表达式是一种在编译时可以完全计算出的表达式。
  4. 常量表达式必须在编译期间得到确定的值,这就意味着在编译时就必须知道它的值。将非类型模板参数定义为常量表达式可以确保其在编译时已经确定并且不会改变。

因此,上述代码我们修改为以下这样即为一个非类型模板参数:

template<class T, size_t N>
class Arry
{
private:
	T _a[N];
};

int main()
{
	Arry<int, 10>aa1;    
	Arry<double, 20>aa2;


	return 0;
}
  • 我们通过调试即可观察到:

  •  跟类型模板参数类似,在这里我们还可以给缺省值,类型模板参数的缺省值是类型,这里非类型模板参数即是常量。
template<class T, size_t N = 20>
class Arry
{
private:
	T _a[N];
};

int main()
{
	Arry<int, 10>aa1;   
	Arry<double, 20>aa2;

	return 0;
}

  • 需要记住一点的是这里的非类型模板参数是一个整形常量,当我们想要去对其进行赋值时此时就会发生报错
template<class T, size_t N = 20>
void func(const T& aa)
{
	N = 10;
}

int main()
{

	func(1);

	return 0;
}

 

  •  double类型此时也会报错,记住一定是【整形常量

  •  除了上述示例中的类模板外,非类型模板参数还可以用于函数模板和变量模板。下面是一个使用非类型模板参数的函数模板示例:
template <int N>
void printNTimes(const string& str) {
    for (int i = 0; i < N; ++i) {
        cout << str << endl;;
    }
}

int main()
{
   
    printNTimes<3>("Hello world"); // 输出三次 "Hello world"
    return 0;
}

【解释】

  1. 上述代码定义了一个打印字符串多次的函数模板 【printNTimes】,其中的模板参数也是一个整数 N
  2. 在函数实现中,我们使用了一个循环来打印字符串 N 次。在使用 【printNTimes 】函数时,我们通过指定模板参数 N 来确定要打印字符串的次数;
  3. 由于 N 是一个非类型模板参数,因此在函数调用时必须指定 N 的常量值。

  • 运行结果如下:

【注意】

  • 1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  • 2. 非类型的模板参数必须在编译期就能确认结果

【综上所诉】

  • 总之,非类型模板参数是C++中另一种有用的模板技术,可以让我们编写更加通用、灵活和高效的代码;
  • 在使用非类型模板参数时,需要注意其类型和常量表达式的限制,并且需要在模板定义和使用时都遵守这些规则。

(二)类模板的特化

1、定义

  1. 在 C++ 中,模板是可以具备通用性的,即针对不同类型的参数,可以用同一份模板代码来生成不同的执行代码,这就是所谓的模板泛化(template generic);
  2. 然而,某些时候,我们需要针对某些特定的类型参数,使用不同的实现代码。这种情况下,我们需要使用模板特化(template specialization)

2、使用场景

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 果,需要特殊处理。

  • 比如:实现了一个专门用来进行小于比较的函数模板
// 函数模板 -- 参数匹配
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; // 可以比较,结果正确
    
     return 0;
}

【解释】

  1. 以上我们对一个日期进行相应的比较(前提大家需要准备一个日期类);
  2. 我们可以发现结果也是正确的

 

  • 但是遇到以下比较时,是否还可以进行正常的比较呢?
    Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,结果错误

【解释 】

  1. 可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果;
  2. 上述示例中,p1指 向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指 针的地址,这就无法达到预期而错误。

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

 //全特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}
  • 当然,我们也可以向下面这样写:
bool Less(Date* left, Date* right)
{
	return *left < *right;
}

 但是,大家不能觉得它没有用,在某些方面它的作用是不容忽视的!!


 模板特化是指针对特定类型参数的一种特殊形式,它可以提供类型特异性的实现。当我们需要提供对某些特殊类型的支持时,可以通过特化来实现。

 ? 特化分为两种:类模板部分特化和函数模板特化

 3、 函数模板特化

函数模板的特化步骤:

  • 1. 必须要先有一个基础的函数模板
  • 2. 关键字template后面接一对空的尖括号 <>
  • 3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  • 4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
     return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* 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;
}

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

bool Less(Date* left, Date* right)
{
     return *left < *right;
}

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


4、类模板特化

1️⃣ 全特化

类模板全特化(full template specialization)指的是对一个类模板进行完全特化,即给定一组类型参数,将该类模板中的所有类型参数都替换成这组给定的类型参数。

例如,对于如下的类模板:

template<class T1, class T2>
class Data
{
public:
     Data() {cout<<"Data<T1, T2>" <<endl;}
private:
     T1 _d1;
     T2 _d2;
};
  • 那么在使用时,就只能使用 Data<int, int> d3; 这个特定类型的类了。

输出我们可以发现:

对于如下的类模板:

template<>
class Data<int, char>
{
public:
	Data() { cout << "Data<int, char>" << endl; }
private:
	int _d1;
	char _d2;
};
  • 那么在使用时,就只能使用 Data<int, char> d4; 这个特定类型的类了。

输出我们可以发现:

需要注意的是:

  • 全特化是针对整个类模板的,而非某个成员函数或成员变量;
  • 同时,全特化并不会影响到其他未特化的类型参数组合,因此在使用时仍需指定正确的类型参数。

2️⃣ 偏特化

类模板偏特化(partial specialization)是指在一个泛型类模板中,对某些模板参数进行特定的限制或约束,以便更好地适应某些特定场景的需求。它可以让我们在针对某些特殊情况的实现中,对泛型算法进行定制化、优化或替换。

比如对于以下模板类:

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

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

  1. 针对部分模板参数的特化:这种方式下,模板类的某些模板参数被特化,而其他模板参数保持不变。例如:
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
 Data() {cout<<"Data<T1, int>" <<endl;}
private:
 T1 _d1;
 int _d2;
}; 

输出我们可以发现:

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

对于如下的类模板:


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

private:
	T1 _d1;
	T2 _d2;
};
  • 那么在使用时,就只能使用 Data<int*, int*> d7;  这个特定类型的类了。

输出我们可以发现: 

 

 对于如下的类模板:

//两个参数偏特化为引用类型
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;
};
  • 那么在使用时,就只能使用 Data<int&, int&> d8(1, 2); 这个特定类型的类了。

 输出我们可以发现: 

需要注意的是:

  • 类模板偏特化不能重载函数,也不能包含成员函数外的非类型成员,否则会导致编译错误。 

5、类模板特化应用示例

 有如下专门用来按照小于比较的类模板Less:

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;
}

解释:

  1. 通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的;
  2. 但是如果待排序元素是指 针,结果就不一定正确。(因为sort最终按照Less模板中方式比较,所以只会比较指针,而不是比较指针指 向空间中内容

此时可以使用类版本特化来处理上述问题:

// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
 bool operator()(Date* x, Date* y) const
 {
     return *x < *y;
 }
};
  • 特化之后,在运行上述代码,就可以得到正确的结果


(三)模板的分离编译

1、什么是模板分离编译?

  1. C++中的模板是一种通用的数据结构或算法,它们不是直接编译成目标代码,而是在实例化时根据类型参数生成相应的代码;
  2. 模板分离编译就是把模板的声明和定义分别放在.h和.cpp文件中,这样在编译时只需要编译一次模板,然后在链接时将所有实例化的代码链接起来。

2、 模板的分离编译

  •  首先,我先给大家明确一点的就是模板是不支持分离编译的,接下来,我简单的举例带大家感受一下。

现在有以下代码:

//func.cpp
#include"func.h"
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

void func()
{
	cout << "void func()" << endl;
}

//test.cpp
#include"func.h"
int main()
{
	Add(1, 2);
	Add(1.0, 2.0);

	return 0;
}


//func.h
#pragma once
#include<iostream>
#include<algorithm>
using namespace std;


template<class T>
T Add(const T& left, const T& right);

void func();

紧接着我们试着去编译看看结果如何:

但是此时我们定义的【func()】函数却可以进行编译 :

 分析如下:

3、 解决方法 

  • 1. 模板定义的位置显式实例化。这种方法不实用,不推荐使用

就像下面这样:

  • 但是这样去写的缺点也很明显,那就是先对十分的麻烦,极大的造成了

  • 2.将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种

  •  此时再去进行编译就可以正常的通过了


总结

到此,关于模板的讲解便到此结束了。接下来,我们简单的总结一下本文都学到了什么.

C++模板是一种灵活而强大的编程工具,它可以帮助我们实现泛型编程,即编写能够适用于多种数据类型的通用代码。

  1. C++模板分为函数模板和类模板两种。

  2. 函数模板是一种通用函数定义,可以接受不同类型的参数,并生成针对每个类型的特定函数定义。函数模板的定义以template关键字开头,后面跟着模板参数列表和函数定义。

  3. 类模板是一种通用类定义,可以接受不同类型的参数,并生成针对每个类型的特定类定义。类模板的定义以template关键字开头,后面跟着模板参数列表和类定义。

  4. 模板参数可以是类型参数、非类型参数或模板参数。类型参数指定了模板类型,非类型参数指定了模板值,而模板参数则允许我们将一个模板作为另一个模板的参数。

  5. 模板特化是指为特定类型或参数值提供特定的模板定义。可以通过显式特化和部分特化两种方式来完成模板特化。

  6. 模板库是C++标准库中的重要组成部分,包含了众多通用的模板类和函数,如STL容器、算法、迭代器等。

总之,C++模板是一种强大而灵活的编程工具,可以帮助我们实现泛型编程,并生成高效、通用的代码。对于C++程序员来说,掌握模板机制是必不可少的技能之一。

以上便是本文的全部内容,感谢各位的观看!!!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。