C++之模板
模板的概念
在将模板之前,我们需要先讲一讲泛型编程。所谓泛型编程就是一种编程范式,其目标就是使代码能够独立于任何特定类型。而C++的模板就是泛型编程的一个重要工具。
下面来举两个泛型编程的例子:
Python是一种动态类型语言,其变量类型在运行是才能确定。
def max_element(lst):
return max(lst)
上面的函数支持传入很多种类型的变量,准确来说,支持传入任何类型的参数,这满足泛型的概念。当然,Python支持更明确的泛型编程类型,这不在本文的讨论范围之内。
将话题扯回到C++,C++是一种静态类型的编程语言,也就是说我们在创建变量的时候必须指定变量的类型。想要其支持泛化的特征,模板是其重要的工具。
第二个例子就是C++的STL库,准确来说使其数据类型。就拿最常用的vector举例,在声明vector类型的变量时,我们需要指定其包含的数据类型是什么,用尖括号来表示。
std::vector<int> sample;
上述的例子中就用 sample存储的数据类型。这就是模板的一个例子,准确来说,这是类模板的一个例子,
类模板的定义看起来像这样:
template <class T>
class vector {
// ...
};
了解了以上的信息后,相信你已经对模板有了一个大致的概念,总而言之:
模板是C++中泛型编程的基础,一个模板是创建一个类或函数的蓝图或者是公式。
函数模板
先从最简单的函数讲起。在C++中使用 template关键字来定义模板,一个函数模板可以这样定义:
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
模板定义以关键字 template开始,后面跟一个模板参数列表,这是一个由逗号分隔的一个或多个模板参数的列表,用尖括号包围起来。
在C++中,关键字 typename用在模板声明中,用于指定一个模板类型的参数,这意味着在模板实例化时,这个参数会被任意类型替换。
在 C++ 模板中,
typename和class关键字在大多数情况下可以互换使用,它们都用来声明类型参数。然而,有一种情况需要注意。当你在模板中使用嵌套类型时,你必须使用typename关键字,而不能使用class关键字。
上面说到模板参数列表是一个可以有多个模板参数的列表,下面是一个多模板参数列表的例子:
template <typename T, class U>
void printPair(T a, U b) {
std::cout << a << ", " << b << std::endl;
}
上面的例子中,可以用两种不同的类型来实例化这个模板,例如:printPair(3, "hello")。
在定义模板的时候,我们隐式或显式的制定了模板实参,例如上面例子中的 T。
编译器处理模板时,会生成不同类型的函数版本(没有用到的类型不会编译),编译器生成的版本通常称为模板的实例。
我们创建的函数模板,也可以返回我们定义的类型参数的数据,例如:
template <typename T>
T fun1(T p)
{
T tmp;
tmp = p;
// ...
return tmp;
}
非类型模板参数
我们可以通过其他关键字来定义非类型模板参数,非类型模板参数表示一个值而非一个类型。例如:
template <typename T, int size>
class Array {
T data[size];
public:
T& operator[](int index) {
return data[index];
}
};
上面是实现一个array类的例子,我们可以通过 Array<int, 10> arr;来声明一个对象。上面的例子用到了重载运算符,之后的文章会详细讲解。
非类型模板参数必须是编译时常量。这意味着你不能用一个运行时确定的值来实例化模板。例如下面这样:
int size = 10;
Array<int, size> arr; // 错误:size 必须是一个编译时常量
小心编译错误
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
这个例子中,传进的变量类型必须能使用 <进行比较才能得出正确的结果,但是如果该类型无法使用 <进行运算,那么将会在实例化这个函数的时候报错。
如何避免这个问题可以在下面的模板特化一部分找到答案。
类模板
类模板是C++中的一种特性,它允许程序员为类定义一种模式,使得类可以处理任何类型的数据。你可以把类模板看作是一种对类型进行参数化的方式。
下面是一个简单的类模板的例子:
template <typename T>
class Box {
private:
T data;
public:
Box(T data) : data(data) {}
T getData() { return data; }
};
在这个例子中,Box 是一个类模板,T 是一个模板参数,它代表了一个类型。Box 类有一个成员变量 data,它的类型是 T。这意味着,你可以用任何类型来创建一个 Box 对象:
Box<int> intBox(10);
Box<std::string> stringBox("Hello, world!");
在这个例子中,Box<int> 是一个具体的类,它的 data 成员的类型是 int。类似地,Box<std::string> 是另一个具体的类,它的 data 成员的类型是 std::string。
类模板可以有多个模板参数。例如,你可以定义一个可以存储两种类型数据的类模板:
template <typename T1, typename T2>
class Pair {
private:
T1 first;
T2 second;
public:
Pair(T1 first, T2 second) : first(first), second(second) {}
T1 getFirst() { return first; }
T2 getSecond() { return second; }
};
在这个例子中,Pair 类有两个模板参数 T1 和 T2,它有两个成员变量,其类型分别是 T1 和 T2。
类模板的一个重要应用是在标准模板库(STL)中。STL 提供了许多模板类,例如 std::vector、std::list、std::map 等等。这些模板类可以用来存储和操作任何类型的数据。
类模板的一个主要优点是它提供了代码复用。你只需要写一次模板代码,就可以用它来处理任何类型的数据。这可以减少代码冗余,并提高代码的可维护性和可读性。
模板特化
函数模板特化
拿之前的例子:
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
如果我们给函数传入 char*类型的参数时,可能会导致此函数出现问题。这时候我们就需要模板特化,特别的为 char*类型构建一个函数,有点类似于函数的重载。模板特化的规则是:先写 template <>,后面的函数实参要指明参数类型,例如:
template <>
int compare(const char *const &v1, const char *const &v2)
{
return std::strcmp(v1, v2);
}
就如上面这个例子,把函数中的参数 T改成了 char *const。当函数匹配到对应的类型时,会自动执行相匹配的函数,例如
std::cout << compare(2, 1) << std::endl; // 执行通用模板
const char *cp1 = "apple", *cp2 = "banana";
std::cout << compare(cp1, cp2) << std::endl; // 执行特化模板
类模板特化
下面是一个类模板特化的例子:
首先,我们定义一个通用的类模板 Box,这个模板有一个 print 成员函数,它打印一条通用的消息:
template <typename T>
class Box {
public:
void print() const {
std::cout << "This is a generic box.\n";
}
};
然后,我们可以为特定类型(如 int)提供一个特化版本。这个特化版本的 print 成员函数打印一条特定的消息:
template <>
class Box<int> {
public:
void print() const {
std::cout << "This is a box of integers.\n";
}
};
这样,当你创建一个 Box<int> 对象并调用其 print 成员函数时,C++ 将会使用特化版本,而不是通用版本。
注意在使用类模板特化时,模板名后面要用尖括号来指定该模板参数的类型,这是因为函数特化可以推导(你要是愿意的话也可以加上尖括号),类模板无法进行推导。
评论区