前言
模板的基本使用
ps:typename 与 class是一样的
学习完C++的函数后,我们可以将一个函数名通过使用不同类型的函数参数,编写多种数据类型的调用而同时使用一个函数名,但是如果我们没有编译某个数据类型的函数参数时,就无法调用,例如:
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
int main()
{
cout << add(1, 2) << endl;
cout << add(1.1, 1.2) << endl;
cout << add(1.2, 1) << endl; //没有符合参数的函数
return 0;
}
那么我们如何 “ 一劳永逸呢”,那就不得不提到C++的模板了
C++ 模板(Templates)是C++语言中的一项强大功能,允许程序员编写泛型代码,提升代码的复用性和可扩展性。在面向对象编程中,我们有时候需要编写针对不同类型的数据进行相似操作的代码。使用模板,我们可以避免重复编写相同逻辑的代码,而是通过参数化类型来泛化代码逻辑。
什么是C++模板?
C++模板可以看作是一种创建泛型函数或类的工具,它允许你定义针对任意类型的操作。通过模板,函数或类可以处理不同数据类型,而不需要为每种数据类型分别重写代码。模板可以分为两类:
- 函数模板(Function Templates)
- 类模板(Class Templates)
函数模板
函数模板允许你编写可以应用于不同类型的函数,而不需要指定具体的数据类型。模板参数通常使用 typename
或 class
关键字表示。
template<typename T>
T add(T a, T b)
{
return a + b;
}
int main()
{
cout << add<int>(1, 2) << endl; 模板实例化
cout << add<float>(1.1, 1.2) << endl;
return 0;
}
在调用时,编译器会自动根据传入参数的类型生成对应的函数版本。
函数模板类型的自动推导
当我们了解函数模板最基本的使用后,他的另外一个我们需要知道的特性同样很重要,函数模板类型的自动推导。当我们调用一个函数模板时,如果我们没有显式地指定模板参数,编译器可以根据传递给函数的实参类型自动推导出模板参数的类型,从而简化调用代码。那也就是说,上述代码调用函数也可以直接传入参数,编译器自动推导
cout << add(1, 2) << endl;
cout << add(1.1, 1.2) << endl;
但是既然知道了可以使用,那什么时候不能使用呢?
template<typename T0
, typename T1
, typename T2
, typename T3
, typename T4>
T2 fun1(T1 v1, T3 v3, T4 v4) 多个模板参数
{
T2 a = T2();
return a;
}
那就需要模板参数对函数参数声明,也就是可以通过具体的参数推导出类型,在上述代码中,作为函数参数的分别是 T1 , T3 , T4,也就是说,这三个参数可以通过具体的参数的类型推导出自己的类型。 至于 T2 虽然是对返回值的修饰,但是编译器不能通过上下文来推断,来推断T2的类型,同时,模板参数实例化必须要按照顺序依次给出,虽然T1作为函数参数类型,可以被推出,但也不能被省略,同样C++语言不支持这样来跳过模板赋值
int a = fun1<double, ,int>(1, 2, 3.0);//语法错误
模板默认值
同样是上述代码,如果我把可以把无法被推导出来的模板参数给上默认值,那么就可以在实例化时不对他传入值,这类似于缺省参数的缺省值
template<typename T0 = float
, typename T1
, typename T2 = double
, typename T3
, typename T4>
T2 fun2(T1 v1, T3 v3, T4 v4)
{
return 0;
}
我们通过自己的想法给出缺省值用于使用,同样,既然给出了缺省值,那么我们就可以不用给出 T0,T2的实例化,那么T1也通过函数参数类型来推导,既然 T2不用实例化,那么T1也不用给出相应的类型实例化
int b = fun2(1, 2, 3.0);
重复模板实例化 namespace 的重要性
之前,我们在不同头文件里定义
text1.h
#pragma once
template<class V>
void fun(V const& a)
{
cout << "2: fun() " << a << endl;
}
void f2()
{
fun(1);
fun(1.2);
}
text2.h
#pragma once
template<class T>
void fun(T const& a)
{
cout << "1: fun() " << a << endl;
}
void f1()
{
fun(1);
fun(1.2);
}
text.cpp
#include<iostream>
using namespace std;
#include"text1.h"
#include"text2.h"
int main()
{
f1();
f2();
return 0;
}
在我的vs2022和gcc(4.8.5) 都已经变成的错误,无法运行,大家也可能在编写自己方法的时候,函数命名和模板参数跟其他.h文件内一样时,就会报错,建议大家将自己的方法写到自己命名的namespace中,但也止不住别人同时展开namespace,造成错误
模板定义与编译分离
在C++中,模板的编译和定义通常是在同一个文件中进行的,这种方式称为模板实例化。但是,为了将模板定义与编译分离,C++98 引入了 export
关键字,用来允许模板的定义在一个单独的实现文件中,而模板的声明可以放在头文件中。然而,export
关键字在模板编程中的支持是非常有限的,并且由于其实现的复杂性,大多数编译器(例如 GCC 和 Clang)都不支持 export
关键字。最终,在 C++11 标准中,export
关键字被移除。
export语法
头文件
#pragma once
export template <typename T>
T add(T a, T b); // 仅声明模板
实现.c文件
#include "mytemplate.h"
export template <typename T>
T add(T a, T b) {
return a + b;
}
#include <iostream>
#include "mytemplate.h"
int main() {
std::cout << add(5, 3) << std::endl; // 调用模板
std::cout << add(2.5, 3.7) << std::endl;
return 0;
}
但是实际情况是:
-
编译器支持:实际上,几乎没有主流的编译器(如 GCC 或 Clang)实现了
export
关键字的支持。唯一一个部分支持export
的编译器是 Comeau C++,它是一个古老的商用编译器,但它已经不再广泛使用。 -
性能和复杂性:
export
模板功能在编译期的实现非常复杂,会增加编译时间,并导致一些不直观的编译错误。因此,编译器制造商们选择不支持该功能。 -
C++11 及其后续版本移除
export
:由于其实现复杂、使用率低,C++ 标准委员会在 C++11 标准中移除了export
关键字。现在,C++11 及之后的标准不再支持export
模板,开发者必须将模板定义放在头文件中,以便在编译时进行实例化。
另外我们还可以通过将函数显示实例化进行模板的定义与编译分离
显式实例化的实现方式
显式实例化可以通过以下步骤实现:
- 在头文件中声明模板类或函数,但不提供完整的实现。
- 在源文件中提供模板的定义并显式实例化特定的模板类型。
- 在需要使用这些模板的其他文件中,只能使用那些显式实例化的类型。
头文件
#pragma once
// 声明模板函数,但不定义它
template <typename T>
T add(T a, T b);
实现.c
// 在这里显式实例化模板,告诉编译器生成特定类型的代码
template int add<int>(int, int);
template double add<double>(double, double);
但是,就算我们显式实例化了这些函数,我们在使用的时候也只能用我们显式实例化的那几个函数,这与我们使用模板的初衷相悖,所以,还是建议, 模板定义与编译不要分离
模板类
同样,在我们设计一个类的时候,同样希望这个类里的结构,或者说是函数,也能使用不同的类型,在C语言时,我们创造链表结构体的时候,想要改变数据类型,就需要宏编程(既不直观,又不方便)
// 定义链表节点结构体和链表结构体的宏
#define DEFINE_LIST(type) \
typedef struct Node_##type { \
type data; \
struct Node_##type* next; \
} Node_##type; \
\
typedef struct List_##type { \
Node_##type* head; \
} List_##type; \
基础语法
template <typename T>
class MyClass {
public:
MyClass(T val) : value(val) {}
T getValue() const { return value; }
private:
T value;
};
我们可以直接将 T 想象成一种类型, 直接用来定义参数,函数参数
类模板的成员函数模板
在某些情况下,类模板中的成员函数可能需要处理与类模板参数无关的类型。这时可以在类模板中定义成员函数模板。
template <typename T>
class MyClass {
public:
template <typename U>
void swap(T& a, U& b) {
U temp = b;
b = a;
a = temp;
}
};
类模板的友元函数模板
有时,类模板的友元函数也需要是模板函数。通过友元函数模板,外部的模板函数可以访问类模板的私有成员。
template <typename T>
class Box {
friend void printBox(const Box<T>&);
private:
T value;
public:
Box(T val) : value(val) {}
};
template <typename T>
void printBox(const Box<T>& box) {
std::cout << "Box value: " << box.value << std::endl;
}
类模板的非类型模板参数
类模板的非类型模板参数是指在模板定义中使用的常量值,而不是类型。非类型模板参数可以是整数、枚举值、指针或其他常量表达式。它们可以用来在编译时确定类的行为或特性
enum color
{
red,
blue
};
template<class T1,enum color>
class Date
{
public:
swaitch()
{
//....
}
private:
};
用的多的还是整形 官方 array类就是用整形,用来确定开辟的类数组大小
异质链表
//异至链表
template<class T, class N>
struct list_node
{
T _val;
N* _next;
list_node(T val, N* next)
:_val(val)
, _next(next)
{}
};
通过这个模板参数传入的 _next类型的参数,链接不同类型的链表
#include <iostream>
// 定义链表节点模板
template<class T, class N>
struct list_node
{
T _val;
N* _next;
list_node(T val, N* next)
: _val(val)
, _next(next)
{}
};
int main() {
// 创建第三个节点(末尾节点,_next 为 nullptr)
list_node<int, void> node3(3, nullptr);
// 创建第二个节点,_next 指向 node3
list_node<int, list_node<int, void>> node2(2, &node3);
// 创建第一个节点,_next 指向 node2
list_node<int, list_node<int, list_node<int, void>>> node1(1, &node2);
//输出值
cout << node1._val << endl
<< node1._next->_val << endl
<< node1._next->_next->_val << endl;
return 0;
}
通过这个方法,我们可以将不同类型的链表链接到一起,但是还是需要头节点的链表进行不断往后访问,如果需要特别长并需要实现像链表的访问,还是需要字节运算 :不同结构体之间的链接-CSDN博客,这种链表的作用更多是保存不同类型的数据。
模板特化
通过模板特化,我们可以将几个特殊的参数类型的模板单独写出来用来匹配需要的方法,例如,参数不同的,需要进一步解应用的,或者是 pair类型需要的第一个first或者是second存放的数据
通用模板类
#include <iostream>
using namespace std;
// 通用模板类
template<class T1, class T2>
class Date {
public:
void printf() {
cout << "Date<T1, T2>" << endl;
}
};
完全特化
// 完全特化:处理 int 和 double 类型
template<>
class Date<int, double> {
public:
void printf() {
cout << "Date<int, double>" << endl;
}
};
偏特化
// 偏特化:处理 double 和其他类型 T1
template<class T1>
class Date<double, T1> {
public:
void printf() {
cout << "Date<double, T1>" << endl;
}
};
//T1 跟double1的位置可以改变
偏特化(指针类型)
// 偏特化:处理 T1* 和 T2* 类型
template<class T1, class T2>
class Date<T1*, T2*> {
public:
void printf() {
cout << "Date<T1*, T2*>" << endl;
}
};
输出
int main() {
// 使用通用模板类
Date<int, char> d1;
d1.printf(); // 输出: Date<T1, T2>
// 使用完全特化类
Date<int, double> d2;
d2.printf(); // 输出: Date<int, double>
// 使用偏特化类
Date<double, float> d3;
d3.printf(); // 输出: Date<double, T1>
// 使用偏特化类(指针类型)
Date<int*, double*> d4;
d4.printf(); // 输出: Date<T1*, T2*>
return 0;
}
当然,这里是关于类模板的博客结尾示例:
总结
类模板是 C++ 中一个强大的特性,它允许程序员创建通用、灵活的类,能够处理不同的数据类型和条件。通过使用类模板,我们可以编写更通用的代码,减少重复,提高代码的可重用性和维护性。
- 模板基础:类模板允许我们定义一个通用的类,并在实例化时指定具体的类型。
- 特化:通过完全特化和偏特化,我们可以为特定的类型或条件提供专门的实现。
- 非类型模板参数:使用非类型模板参数(如常量、枚举、指针)能够在编译时确定类的行为,使得类模板更加灵活。
- 实践应用:通过枚举作为非类型模板参数,我们可以根据不同的条件来定制类的功能,例如日志记录、配置管理等。
上述所有应用都是C++模板的最最最基础,模板还可以干很多很多事情,事实证明,C++模板编程是很深奥的,也被称为 C++模板元编程,但是代码可读性太差,如果有喜欢的朋友可以去阅读书籍 深入实践C++模板编程
和 C++模板元编程的博客C++模板元编程(C++ template metaprogramming) - liangliangh - 博客园 (cnblogs.com)