C++:模板

简介: C++:模板



在讲解模板前,我提出一个问题:

如何实现一个通用的swap交换函数?

也许你可以这样:

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

把每种类型都进行重载,写出n多种交换函数。

但这很明显是一个费力不讨好的方法,不仅会让代码冗余重复,而且写出这样的代码,也会耗费精力。

那么有没有一种办法,我们给编译器一个模板,编译器自动生成函数?

C++就样提供了模板,而C++最重要的STL库,也就是起源于模板的。

模板分为函数模板与类模板,我们先通过函数模板来了解模板的大部分规则:

函数模板

功能:

函数模板代表了一个函数家族,在使用时被参数化,根据实参的类型生成特定类型的版本

也就是说,我们可以通过一个函数模板,让编译器生成一整个同类型的函数家族。

语法:

template <typename T1, typename T2 ......>
template <class T1, class T2 ......>

以上两种都是创建模板的方式,classtypename在里面是一样的功能,没有区别。在STL中多用class,所以本博客也以class为主。

而被classtypename定义是模板参数,它可以被任意类型代替。如果你不能理解,我们不妨看看示例。

示例:

template <class T>
T Add(T x, T y)
{
  return x + y;
}

以上就是一个函数模板,T是一个模板参数,它代表一个类型。如果T是int,那么以上函数就是:

int Add(int x, int y)
{
  return x + y;
}

这个函数是不是很熟悉了?

也就是说,T是可以被替换的类型,那么我们要如何确定这个T的类型?

编译器会根据调用函数时传入的参数,自动判断类型

比如以下调用:

Add(1, 5);
Add(3.0, 5.0);

对于Add(1, 5);,其两个参数都是int类型,那么此时模板就会生成一个int类型的Add函数。

对于Add(3.0, 5.0);,其两个参数都是double类型,此时模板就会生成一个double类型的Add函数。

以此类推,我们不论想要多少种类型,只需要传入参数,让编译器自动识别,而我们只需要写一个模板,就可以衍生出无数种函数,这就是模板的优势。

那么我们现在来实现一下一开始swap函数的模板:

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

不过要注意,模板只是一个蓝图,本身不是函数,当我们传入指定类型参数,其就会生成相应的函数。


显式实例化

再回到刚刚的Add函数模板。

template <class T>
T Add(T x, T y)
{
  return x + y;
}

如果我们传入两个不同类型的参数怎么办?

比如这样:

int a = 5;
double b = 3.0;
Add(a, b);

请问这个模板是转化为double类型好呢,还是转化为int类型好呢?

对编译器来说,这就是一个大问题了,如果转化错误了,编译器就要背黑锅。所以遇到这种情况,编译器不会为我们做决定,而是报错,必须由程序员指明要用哪一种类型的模板。

比如使用强制类型转换:

int a = 5;
double b = 3.0;
Add(a, (int)b);
Add((double)a, b);

上述代码,Add(a, (int)b);将b转化为了int,此时模板推演出int类型的函数;而Add((double)a, b);将a转化为了double类型,此时模板推演出double类型的参数。

此外,我们还可以使用显式实例化的方式:

显式实例化,就是在使用模板时,明确的告诉模板,要用什么类型。

语法:

函数名 <类型> (参数);

比如:

int a = 5;
double b = 3.0;
Add<int> (a, b);
Add<double> (a, b);

Add<int> (a, b);此代码就是推演出int类型的函数;而Add<double> (a, b);就是推演出double类型的函数。


模板参数缺省

在设置模板参数时,可以设置缺省值,在显式实例化时,对于没有指明的模板参数,会被推演为缺省值。

看到以下模板:

template <class T1, class T2>
void func(T1 a)
{
  T2 b = 5;
  cout << a / b;
}

这个模板中,函数func只有一个形参,而T2这个模板参数不在形参中,而是用于定义b这个变量了。此时T2是无法根据函数的实参推演出来的,必须显式实例化中指明。

比如这样:

func<int, int>(8);

此时T1int类型,T2int类型,执行整数除法8/5=1

接下来我们给模板参数缺省值试试:

template <class T1, class T2 = double>
void func(T1 a)
{
  T2 b = 5;
  cout << a / b;
}

此处我们给了T2一个缺省值double,也就是说我们不传入第二个模板参数,T2就会被推演为缺省值double

func<int>(8);
func(8);

对于func<int>(8);,我们只传了一个模板参数,此时T2就会被推演为缺省值double,变量b就是double类型了,此时执行小数除法8 / 5.0 = 1.6

对于func(8);,我们没有进行显式实例化,此时对于T1,由于我们传入了参数aint类型,此时T1被推演为int,而T2得到缺省值double,执行小数除法8 / 5.0 = 1.6


参数匹配规则

模板本身不是一个函数,所以同名的函数和模板是可以共存的。

比如这样:

template <class T>
T func(T x, T y)
{
  return x + y;
}
int func(int x, int y)
{
  return x + y;
}

以上代码中我们创建了一个Add的模板,一个Addint类型函数。

那么我们调用函数时,会这么调用呢?

调用函数时,如果函数有现成的,完全匹配的函数,那么不会调用模板

如果我们这样调用函数:

Add(1, 2);

这个调用,两个参数都是int类型,而我们刚好写了一个两个参数都是int类型的函数,那么此次调用就不会调用模板,而是直接用我们写过的函数。

调用函数时,如果可以通过模板产生更加匹配的函数,那么会调用模板进行推演

如果我们这样调用函数:

Add(1.0, 2.0);

此时两个参数都是double类型,如果不存在模板的话,double就会被转化为int,然后调用int,int的函数。但是由于模板存在,可以推演出更加匹配从函数double,doouble类型。所以此次调用会调用模板。


类模板

类模板的特性与函数模板几乎一致,此处不额外讲解了,只讲解类模板的特殊的地方。

语法:

template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};

先简单为大家展示一个类模板:

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

这就是一个stack类的模板,有了这个模板,我们的栈就可以存放intdouble等等的其他类型了。

我们的模板参数为T,由于类没有传参的概念,不能通过参数来推演类型,所以一般而言类的模板都是要显式实例化的

比如:

stack<int> s1;
stack<double> s2;

类名与类型

通过模板创建的类,其类名与类型也有所不同,接下来我们看看规则:

在一般的类中,类的类名和类型符号相同

比如stack的类,其类名为stack,类型也为stack

而在类模板中,不能单纯的将类名作为类型了,比如:

stack<int> s1;
stack<double> s2;

请问s1和s2的类型都是stack吗?

s1明明是用int推演的类,s2是用double推演的类,两者有很大的区别,如果都是stack类,后续如何区分?

所以我们用了其他规则来修饰这个类型符号,从而区分开同一个模板推演出来的不同类型。

类型 = 类名<模板参数>

对于stack<int> s1;其类名为stack,类型为stack<int>

对于stack<double> s2;其类名为stack,类型为stack<double>;


类成员的声明定义分离

当我们希望把一些类中的成员定义在类的外部时,那就需要声明和定义分离。

假设我们希望分离析构函数~stack

对于一般的类,我们会这样分离:

class stack
{
public:
  stack(size_t capacity = 10)
  :_pData(new int[capacity])
  ,_size(0)
  ,_capacity(capacity)
  {}
  
  ~stack();//声明
  
private:
  int* _pData;
  size_t _size;
  size_t _capacity;
};
stack::~stack()
{
  //函数体
}

首先要用类型::函数名来限定作用域,然后再开始定义函数。

所以我们的类模板也要类型::函数名来限定作用域。类模板的类型刚刚介绍过,就是stack<T>,所以函数的声明应该这样写:

stack<T>::~stack()
{
  //函数体
}

但是这还不是一个合法的声明。

对于类模板,当在类外定义函数时,要添加模板参数列表。

也就是说要这样:

template <class T>
stack<T>::~stack()
{
  //函数体
}

这才是一个模板类的成员函数声明。


非类型模板参数

我们的模板参数也可以不是一个类型,而是一个数值

对于指定类型的参数,我们称为类型形参,比如int,double。

对于一个数值的参数,我们称为非类型形参。

比如以下类模板:

template <class T, int N>
class Array
{
public:
private:
  T _arr[N];
};

我们在模板参数列表中有一个类T,一个int类型的N,此时T就是类型形参,N就是非类型形参。

在定义类时,可以通过这个非类型参数,为这个类传入一些值:

Array<int, 10> a;
Array<double, 20> b;

对于a这个对象,我们在创建时,为T传入了intN传入了10。那么经过初始化,_arr就指向了10int的数组。

对于b这个对象,我们在创建时,为T传入了doubleN传入了20,那么经过初始化,_arr就指向了20double类型的数组。

注意:非类型模板参数必须是整型,bool,char类型的常量


模板特化

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

比如以下代码:

template<class T>
bool Less(T x, T y)
{
  return x < y;
}
int main()
{
  int* p1 = 5;
  int* p2 = 10;
  cout << Less(p1, p2) << endl;
  return 0;
}

这个函数模板中,我们用Less来比大小,我们此时传入了两个指针p1p2,原本的意图是通过指针来比较数字5和10的大小。但是当传入后,我们比较的是p1 < p2,也就是对两个指针比大小了,这不符合我们的预期。也就是说在面对指针的时候,我们需要特殊处理,这就需要模板特化了。

模板特化的功能就是:

在原模版的基础上,针对特殊类型进行特殊化的实现方式

其分为函数模板的特化与类模板的特化:


函数模板特化

我们先看到一个函数模板特化,再讲解语法:

//基础模板
template<class T>
bool Less(T x, T y)
{
  return x < y;
}
//模板特化
template<>
bool Less<int*>(int* x, int* y)
{
  return *x < *y;
}

第一段代码是一般的函数模板,而第二段是对int进行了特化的版本,当我们传入参数类型为int时,就会调用这个特化版本,执行*x < *y,先解引用再比较。

那么这个模板特化有什么特点呢?

首先,我们将T特化为了int*,所以T不再是一个需要推演的参数了,此时将T从模板参数列表中抽离出来,改为int*放到函数名Less后面,用尖括号括起来,然后把函数参数中所有的T改为特化后的int*

模板特化要满足以下语法:

  1. 必须存在一个基础模板
  2. 对于特化版本,template后面的<>内部不写被特化的模板参数
  3. 对于特化版本,在函数名后跟上<>,内部指定特化类型
  4. 将特化前的模板参数改为特化后的具体类型

但是函数模板是一个没有必要的东西,因为相比于对模板进行特化,不如直接重载一个函数,模板特化在类模板中较为有用。

比如这样:

bool Less(int* x, int* y)
{
  return *x < *y;
}

可以达到一样的效果,而且无需繁杂的语法。

模板特化的主要用处体现在类模板特化上,函数并不推荐使用这个模板特化。


类模板特化

类模板特化的语法和刚才是一样的:

模板特化要满足以下语法:

  1. 必须存在一个基础模板
  2. 对于特化版本,template后面的<>内部不写被特化的模板参数
  3. 对于特化版本,在函数名后跟上<>,内部指定特化类型
  4. 将特化前的模板参数改为特化后的具体类型

类模板特化分为全特化和偏特化。


全特化

全特化是指将模板参数的所有参数都确定下来

比如以下案例:

//基础模板
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;
};

此处我们将T1T2两个参数都设立了特化,这就叫做全特化。

只有模板参数第一个值为int,第二个参数为char,调用此类。

比如:

Data<int, char> d1;

这里的d1就是一个模板特化创造出来的类对象。


偏特化

偏特化是指并没有把模板参数确定下来,但是对满足特定条件的模板参数,执行特化版本

偏特化分为部分特化和限制特化:

部分特化

部分特化是只将一部分参数特化

比如以下案例:

//基础模板
template<class T1, class T2>
class Data
{
public:
  Data() {cout<<"Data<T1, T2>" <<endl;} 
private:
  T1 _d1;
  T2 _d2;
};
//模板特化
template<class T1>
class Data<T1, char>
{
public:
  Data() {cout<<"Data<T1, char>" <<endl;}
private:
  T1 _d1;
  char _d2;
};

以上的第二段代码就是一个部分特化,其只特化了第二个模板参数为char,只有当第二个参数为char类型,不论第一个参数类型是什么,都会调用特化版本的类了。

由于T1没有被确定下来,仍然需要推演,所以第一行的模板参数列表保留T1

比如:

Data<int, char> d2;
Data<double, char> d3;

这里的d2d3都是通过模板特化创建出来的对象,因为它们满足第二个模板参数是char类型。


限制特化

限制特化是对参数进行条件限制,但是没有把参数类型确定下来

比如以下案例:

//基础模板
template<class T1, class T2>
class Data
{
public:
  Data() {cout<<"Data<T1, T2>" <<endl;} 
private:
  T1 _d1;
  T2 _d2;
};
//模板特化
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
  Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:
  T1 _d1;
  T2 _d2;
};

此处<T1*, T2*>是限定:当T1T2为指针是,调用此模板特化。

也就是说,这个过程中,T1T2的类型是不确定的,任然需要推演,所以第一行的模板参数列表保留了T1T2

而这样对模板参数进行限制,就是限制特化了。


相关文章
|
3月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
104 0
|
3月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
75 0
|
11月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
222 10
|
编译器 C++
【C++】——初识模板
【C++】——初识模板
【C++】——初识模板
|
6月前
|
编译器 C++
模板(C++)
本内容主要讲解了C++中的函数模板与类模板。函数模板是一个与类型无关的函数家族,使用时根据实参类型生成特定版本,其定义可用`typename`或`class`作为关键字。函数模板实例化分为隐式和显式,前者由编译器推导类型,后者手动指定类型。同时,非模板函数优先于同名模板函数调用,且模板函数不支持自动类型转换。类模板则通过在类名后加`&lt;&gt;`指定类型实例化,生成具体类。最后,语录鼓励大家继续努力,技术不断进步!
|
7月前
|
编译器 C++
㉿㉿㉿c++模板的初阶(通俗易懂简化版)㉿㉿㉿
㉿㉿㉿c++模板的初阶(通俗易懂简化版)㉿㉿㉿
|
7月前
|
安全 C++
【c++】模板详解(2)
本文深入探讨了C++模板的高级特性,包括非类型模板参数、模板特化和模板分离编译。通过具体代码示例,详细讲解了非类型参数的应用场景及其限制,函数模板和类模板的特化方式,以及分离编译时可能出现的链接错误及解决方案。最后总结了模板的优点如提高代码复用性和类型安全,以及缺点如增加编译时间和代码复杂度。通过本文的学习,读者可以进一步加深对C++模板的理解并灵活应用于实际编程中。
91 0
|
7月前
|
存储 安全 算法
深入理解C++模板编程:从基础到进阶
在C++编程中,模板是实现泛型编程的关键工具。模板使得代码能够适用于不同的数据类型,极大地提升了代码复用性、灵活性和可维护性。本文将深入探讨模板编程的基础知识,包括函数模板和类模板的定义、使用、以及它们的实例化和匹配规则。
|
10月前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
308 4
|
10月前
|
算法 编译器 C++
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
123 3