文章目录
前言
本章参考了C++ 模板常见特性(函数模板、类模板),和《C++ Primer Plus 6th中文版》
- 函数模板
- 类模板
使用「模板」的特性设计,实际上也就是「泛型」程序设计。
一、函数模板
函数模板是一种工具,允许程序员编写与数据类型无关的代码。函数模板可以用来创建可用于不同数据类型的函数,而无需为每种类型编写单独的函数。这种方法提高了代码的可重用性和灵活性。
函数模板通过使用泛型来定义函数的功能,让你可以用相同的函数体处理不同的数据类型。在编译时,编译器根据函数模板生成具体类型的函数定义。这个过程称为模板实例化。
typename
关键字使得参数AnyType
表示类型这一点更为明显;然而,有大量代码库是使用关键字class
开发的。在这种上下文中,这两个关键字是等价的。
基本语法
函数模板的声明以关键字 template 开始,后跟模板参数列表,这些参数通常是类型参数。类型参数在函数定义中用作占位符,表示将由实际的数据类型替换。
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
实例化
函数模板的实例化可以是显式的或隐式的:
显式实例化:你指定模板参数的类型。
隐式实例化:编译器根据提供给函数的参数类型自动推导出模板参数的类型。
int main() {
int x = 5, y = 10;
double n = 3.5, m = 1.2;
// 隐式实例化
std::cout << "Max int: " << max(x, y) << std::endl;
std::cout << "Max double: " << max(n, m) << std::endl;
// 显式实例化
std::cout << "Max int using explicit: " << max<int>(x, y) << std::endl;
std::cout << "Max double using explicit: " << max<double>(n, m) << std::endl;
return 0;
}
注意事项:
- 模板代码在使用前不会被编译,只有在模板实例化时才进行编译。
- 因为模板会为每种数据类型生成不同的函数实例,所以使用模板可能会增加编译后的程序大小。
具体例子
01 变量交换函数模板
template <typename T>
void Swap(T & x,T & y)
{
T tmp = x;
x = y;
y = tmp;
}
02 查询数组最大值函数模板
// 求数组最大元素的MaxElement函数模板
template <class T>
T MaxElement(T a[], int size) // size是数组元素个数
{
T tmpMax = a[0];
for(int i = 1;i < size;++i)
{
if(tmpMax < a[i])
{
tmpMax = a[i];
}
}
return tmpMax;
}
03 多个类型参数模板函数
函数模板中,可以不止一个类型的参数:
template <typename T1, typename T2>
T2 MyFun(T1 arg1, T2 arg2)
{
cout<< arg1 << " "<< arg2<<endl;
return arg2;
}
//T1 是传入的第一种任意变量类型,T2 是传入的第二种任意变量类型。
04 函数模板的重载
函数模板可以重载,只要它们的形参表或类型参数表不同即可。
// 模板函数 1
template<typename T1, typename T2>
void print(T1 arg1, T2 arg2)
{
cout<< arg1 << " "<< arg2<<endl;
}
// 模板函数 2
template<typename T>
void print(T arg1, T arg2)
{
cout<< arg1 << " "<< arg2<<endl;
}
// 模板函数 3
template<typename T,typename T2>
void print(T arg1, T arg2)
{
cout<< arg1 << " "<< arg2<<endl;
}
上面都是 print(参数1, 参数2) 模板函数的重载,因为「形参表」或「类型参数表」名字不同。
模板函数 2 和模板函数 3 看似相似,但它们有一个关键的区别,那就是在模板参数的定义上。尽管这两个模板函数的行为在某些情况下可能相同,关键的区别在于类型参数的泛化程度和模板的调用方式。
模板2:这个函数模板只使用了一个类型参数 T,这意味着 arg1 和 arg2 必须是相同的类型。只有当两个参数类型完全一致时,这个模板才会被实例化。
模板3:这个函数模板使用了两个类型参数 T 和 T2,这允许 arg1 和 arg2 是不同的类型。这种模板的灵活性更高,因为它不要求两个参数具有相同的类型
二者区别于用途:
类型的灵活性:模板函数 3 比模板函数 2 提供了更高的类型灵活性,因为它允许两个不同类型的参数。这在处理不同类型的数据时非常有用。
实例化和重载解析:在函数调用时,如果提供了两个不同类型的参数,模板函数 3 将被优先选择,因为它提供了精确匹配的重载版本。如果两个参数类型相同,那么模板函数 2 和模板函数 3 都可以匹配,但通常编译器会优先选择最特化的版本,即模板参数数量最少的版本。
05 函数模板和函数的次序
在有多个函数和函数模板名字相同的情况下,编译器如下规则处理一条函数调用语句: 1. 先找参数完全匹配的普通函数(非由模板实例化而得的函数); 2. 再找参数完全匹配的模板函数; 3. 再找实参数经过自动类型转换后能够匹配的普通函数; 4. 上面的都找不到,则报错。
// 模板函数 - 1个参数类型
template <class T>
T Max(T a, T b)
{
cout << "TemplateMax" <<endl; return 0;
}
// 模板函数 - 2个参数类型
template <class T, class T2>
T Max(T a, T2 b)
{
cout << "TemplateMax2" <<endl; return 0;
}
// 普通函数
double Max(double a, double b)
{
cout << "MyMax" << endl;
return 0;
}
int main()
{
int i=4, j=5;
// 输出MyMax - 匹配普通函数
Max( 1.2, 3.4 );
//输出TemplateMax - 匹配参数一样的模板函
Max( i, j );
//输出TemplateMax2 - 匹配参数类型不同的模板函数
Max( 1.2, 3 );
return 0;
}
匹配模板函数时,当模板函数只有一个参数类型时,传入了不同的参数类型,是不进行类型自动转换,具体例子如下:
// 模板函数 - 1个参数类型
template<class T>
T myFunction( T arg1, T arg2)
{
cout<<arg1<<" "<<arg2<<"\n";
return arg1;
}
...
// OK :替换 T 为 int 类型
myFunction( 5, 7);
// OK :替换 T 为 double 类型
myFunction(5.8, 8.4);
// error :没有匹配到myFunction(int, double)函数
myFunction(5, 8.4);
二、类模板
提到STL标准模板库必定会提到类模板。
2.1定义类模板
类似于函数模板,类模板的代码定义:
template <typename Type> // 模板类型
template <class Type> // 模板类
template
告诉编译器,将要定义一个模板,尖括号中的内容相当于函数的参数列表。- 关键字
class
看作变量的类型名,变量接受类型作为其值,把Type看作是该变量的名称。 - Type 表示一个通用的类型说明符。在使用模板时,使用实际的类型替换它。
如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。
模板类不是函数,不能单独编译。
模板必须和特定的模板实例化
请求一起使用。一般是使将所有模板信息放在一个头文件
中。
2.2 使用模板了类
声明模板类的对象
,必须显式地提供所需的类型
。这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数。但是类模板不行
Stack<int> kernels; // 存储int类型的栈类
Stack<string> colonels; // 存储string类型的栈类
2.3 深入探讨模板类
可以将内置类型或类对象用作类模板Stack的类型。指针可以吗?例如,可以使用char指针替换string对象吗?
毕竟,这种指针是处理C-风格字符串的内置方式。答案是可以创建指针栈,但如果不对程序做重大修改,将无法很好地工作。编译器可以创建
类,但使用效果如何就因人而异了。
因为你虽然用指针代替string,但是你没有为其开辟内存!!!编译虽然会通过,但是运行时程序会崩溃。
使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。把这些指针放在栈中是有意义的,因为每
个指针都将指向不同的字符串。注意,创建不同指针是调用程序的职责,而不是栈的职责。栈的任务是管理指针,而不是创建指针。
详情见书《C++ Primer Plus 6th中文版》P465
指针这块用得确实很少,所以不补充这块内容。等遇到有用的再另外填坑
2.4 数组模板示例和非类型参数
探讨探讨一些非类型(或表达式)参数以及如何使用数组来处理继承族。
首先介绍一个允许指定数组大小的简单数组模板:
- 一种方法是在类中使用动态数组和构造函数参数来提供元素数目。
- 另一种方法是使用模板参数来提供常规数组的大小,C++11新增的模板array就是这样做的。
// arraytp.h -- Array Template
#ifndef ARRAYTP_H_
#define ARRAYTP_H_
#include <iostream>
#include <cstdlib>
template <class T, int n>
class ArrayTP{
private:
T ar[n];
public:
ArrayTP() {};
explicit ArrayTP(const T & v);
virtual T & operator[](int i);
virtual T operator[](int i) const;
};
template <class T, int n>
ArrayTP<T,n>::ArrayTP(const T & v){
for (int i = 0; i< n; i++) ar[i] = v;
}
template <class T, int n>
T & ArrayTP<T, n>::operator[](int i){
if (i < 0 || i >= n){
std::cerr << "Error in array limits: " << i << " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i]
}
template <class T, int n>
T & ArrayTP<T, n>::operator[](int i) const {
if (i < 0 || i >= n){
std::cerr << "Error in array limits: " << i << " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i]
}
#endif
这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式(expression)参数。
表达式参数方法的主要缺点是,每种数组大小都将生成自己的模板。也就是说,下面的声明将生成两个独立的类声明:
ArrayTP<double, 12> t1;
ArrayTP<double, 13> t2;
但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数:
ArrayTP<int> t1(12);
ArrayTP<int> t1(13);
2.5模板多功能性
模板类的三个功能:
- 用作基类
template <typename Type>
class GrowArray : public Array <Type> {...};
-
用作组件类
-
常规组件
template <typename Tp> class Stack { Array<Tp> ar; // use an Array<> as a component ... };
-
模板可以包含多个类型参数。
template <class T1, class T2> class Pair { private: T1 a; T2 b; public: T1 & first(); T2 & second(); ... };
-
-
递归使用模板
ArrayTP<ArrayTP<int, 5>, 10> twodee // 与下面常规数组声明等价 int twodee[10][5]
在模板语法中,维的顺序与等价的二维数组相反。
-
默认类型模板参数
-
可以为类型参数提供默认值
template <class T1, class T2 = int> class Topo {...}; // 如果省略了T2的值,编译器会默认使用 int
-
不能为
函数模板参数
提供默认值。但可为非类型参数
提供默认值。
-
2.6 模板的具体化
隐式实例化
、显式实例化
和显式具体化
,统称为 具体化
。
模板
使用 泛型
的方式描述类
,具体化
是使用具体化的类型
生成类声明
。
-
隐式实例化:
当模板函数或模板类被使用时,如果还没有相应的实例,编译器会根据提供给模板的实际类型参数自动生成一个特定的实例。
-
声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。
ArrayTP <int, 100> stuff;
-
-
显式实例化:
这是一种手动指示编译器为特定类型生成模板实例的方法,无需等待模板的实际使用。这样做的好处是可以减少编译时间,因为编译器不需要在每次遇到新的类型时重新实例化模板。
-
当使用关键字 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化。
-
声明必须位于模板定义所在的名称空间中。
template class ArrayTP<string, 100>; // 虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)
-
-
显式具体化:
当模板的一般实现不适用于某些特定类型时,可以为这些类型提供特殊的实现。这称为模板的显式具体化,也常简称为模板特化。这使得模板在处理特定类型时可以有不同的行为。-
格式如下:
template <> class Classname <specialized-type-name> {...}; // 显式具体化的例子 template <> class Pair<int,int> {...};
-
-
部分具体化
是一种特殊的模板特化形式,用于对模板的一部分参数进行具体化,而不是全部参数。部分具体化非常适用于模板类,因为它允许开发者针对某些特定情况提供更具体的实现,同时保持模板的泛用性。不过,需要注意的是,C++ 允许对模板类进行部分具体化,但对于模板函数则只能进行全特化,不能进行部分具体化。-
部分限制模板的通用性。
// 通用模板 template <class T1, class T2> class Pair {...}; // template <class T1> class Pair<T1,int> {...}; // T1 保持不变,但是T2 被具体化为 int
template <typename T1, typename T2> class MyClass; // 通用模板 // 部分具体化:针对第二个类型参数是 int 的情况 template <typename T1> class MyClass<T1, int> { // 实现细节 };
-
关键字
template
后面的<>声明
的是没有被具体化的类型参数。 -
如果多个模板可供选择,编译器将优先使用具体化程度最高的模板。
-
如果提供的类型不是指针,则编译器将使用通用版本。
-
如果提供的是指针,则编译器将使用指针具体化版本
-
理解显式实例化
显式实例化的主要目的是:
- 减少编译时间:因为编译器不必为每个翻译单元中的每次使用重复生成模板的实例。
- 避免链接错误:确保模板的每个实例在程序的任何地方被一致地使用。
- 控制模板实例化:有助于控制模板代码膨胀和管理模板生成的二进制大小。
2.7 成员模板
模板可用作结构、类或模板类的成员。要完全实现STL的设计,必须使用这项特性。这种功能使得你的类可以具备处理不同数据类型的灵活性,而不必为每种数据类型重写方法或类。
// template member
#include <iostream>
using std::out;
using std::cin;
using std::endl;
template <typename T>
class beta
{
private:
template <typename V> //
class hold
{
private:
V val;
public:
hold(V v = 0) : val(v) {}
void show() const {cout << val << endl; }
V value() const {return val;}
};
hold<T> q; // template object
hold<int> n; // template object
public:
beta(T t, int i) : q(t), n(i) {}
template<typename U> // template method
U blab(U u,T t) {return (n.Value() + q.Value() * u / t;}
void Show() const {q.Show(); n.Show();}
};
hold模板是在私有部分声明的,因此只能在beta类中访问它。beta类使用hold模板声明了两个数据成员:
hold<T> q; // template object
hold<int> n; // template object
blab( )方法的U类型由该方法被调用时的参数值显式确定,T类型由对象的实例化类型确定。
模板可以嵌套,所以语法为:
template <typename T>
template <typename V>
// 不能使用如下语法
template<typename T, typename V>
嵌套模板参数(template <typename T> template <typename V>
)通常用于类或结构体内部,其中模板成员需要额外的类型参数。
多类型模板参数(template<typename T, typename V>
)用于定义同时需要多个类型参数的函数或类。
2.8 使用模板作为参数
模板可以包含类型参数(如typename T)和非类型参数(如int n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现STL。
template <template <typename T> class Thing>
class Grab
- 模板参数是
template<typename T> class Thing
,其中template class 是类型,Thing 是参数。
以下是一个示例,演示如何使用模板作为参数来设计一个容器包装器类,该类可以与不同的容器类型一起工作,如 std::vector、std::list 等。
我们将创建一个 ContainerWrapper 类,它接受两个参数:一个是元素类型,另一个是容器类型。容器类型本身是一个模板,这就是模板模板参数的使用场景。
#include <iostream>
#include <vector>
#include <list>
// 定义模板模板参数
template <template <typename, typename> class ContainerType, typename ValueType, typename Allocator = std::allocator<ValueType>>
class ContainerWrapper {
public:
ContainerType<ValueType, Allocator> container;
void add(const ValueType& value) {
container.push_back(value);
}
void display() const {
for (auto& elem : container) {
std::cout << elem << ' ';
}
std::cout << std::endl;
}
};
int main() {
// 使用 vector 作为容器
ContainerWrapper<std::vector, int> vectorWrapper;
vectorWrapper.add(10);
vectorWrapper.add(20);
vectorWrapper.add(30);
vectorWrapper.display(); // 输出: 10 20 30
// 使用 list 作为容器
ContainerWrapper<std::list, int> listWrapper;
listWrapper.add(40);
listWrapper.add(50);
listWrapper.add(60);
listWrapper.display(); // 输出: 40 50 60
}
2.9 模板类和友元
模板类声明也可以友元。模板的友元分为3类:
-
非模板友元
- 在模板类中声明的一个常规友元函数,称为模板所有实例化的友元。例如,它将是类hasFriend和HasFriend的友元。
template<class T> class HasFriend { public: friend void counts(); ... };
- 当为友元函数提供模板类参数,必须指明具体化。
template <class T> class HasFriend { friend void report(HasFriend<T> &);// 具体化之后,就会变成约束模板友元 };
-
约束模板友元:友元的类型取决于类被实例化时的类型。
-
在
类外声明
的模板具体化
。 -
2个步骤:
- 在类定义的前面声明每个模板函数
template <typename T> void counts(); template <typename T> void reports(T &);
- 在函数中再次将模板声明为友元。根据类模板参数的类型声明具体化。
template <typename TT> class HasFriendT { friend void counts<TT>(); friend void report <HasFriendT<TT> &>; ... };
-
-
模板类的非约束模板友元函数:即友元的所有具体化都是类的每一个具体化的友元。
在类内部
声明模板,可以创建非约束友元函数
。
template <typename T>
class ManyFriend
{
template <typename C,typename D> friend void show2(C &, D &)
...
};
// 实现使用方式
template <typename C, typename D> void show2(C & c, D & d)
{
...
}
2.10 模板别名(C++11)
- 使用
typedef
为模板具体化指定别名
typedef std::array<double,12> arrd;
arrd gallons; // gallon type : std::array<double,12>
- 使用
using name = type
于模板
template <typename T>
using arrtype = std::array<T,12>; // arrtype定义为一个模板别名
arrtype<double> gallons; //gallons is type std::array<double, 12>
using
用于非模板时与常规的typedef等价。
总结
- 函数模板
- 类模板:基本上就是围绕着如何实例化展开,以及一些模板的高级用法
参考文献
- 《C++ Primer Plus 6th中文版》
- https://github.com/SolerHo/cpp-Primer-Plus-6e-Notes/tree/master
- C++ 模板常见特性(函数模板、类模板)(小林coding)