本节我们将要介绍模板的深入知识。
文章目录
前言
一、非类型模板参数
1.1 非类型模板参数的特点
1.2 非类型模板参数的使用场景
1.3 非类型模板参数的使用实例
二、模板特例化
2.1 模板特例化的概念
2.2 函数模板特例化
2.3 函数模板特例化规则
2.4 完全特例化和偏特例化
1.完全特例化:
2. 偏特例化
3. 完全特例化与偏特例化的区别
2.5 类模板特例化
三、模板的分离编译
3.1 分离编译
3.2 函数分离编译
3.3 模板的分离编译
四、模板的总结
前言
我们在刚开始学习C++不久就接触到了模板,刚开始我们为了后面进度的推进,仅仅是学习了一些皮毛,经过STL的各种容器的学习,我们逐渐熟悉了模板,在这节我们将要对模板进一步学习。
一、非类型模板参数
模板参数分为类型模板参数和非类型模板参数。类型模板参数即出现在模板参数列表中,出现在class或typename后面的参数类型名称;非类型模板参数表示的是一个值并非是一个类型,我们通过定义一个特定的类型名而非关键字class或者typename来指定非参数类型。
当我们定义了一个非类型模板参数的模板被进行实例化后,非类型参数就会被一个用户提供的或编译器推断出的值所替代,这些值必须是常量表达式。(其实非类型模板参数在我们编译期间就会被替换了,因此我们要在编译期间就要确定好)
1.1 非类型模板参数的特点
-
常量要求:非类型模板参数必须是常量表达式,即编译时可以确定的常量。这些常量值可以是整数、枚举类型、指针、引用等。至于浮点数,类对象以及字符串是不允许作为非类型模板参数的。
-
类型限制:非类型模板参数的类型限制比较严格,只能是可以在编译时计算的常量。常见的类型包括:
- 整型常量:如
int
,char
,long
等 - 枚举类型:可以将枚举值作为非类型模板参数
- 指针:常量指针或指向常量的指针
- 引用:常量引用
- 类类型的静态常量成员:可以传递类中定义的静态常量作为模板参数
- 整型常量:如
-
模板实例化:根据不同的非类型模板参数,C++ 编译器会生成不同的模板实例。例如,可以根据数组的大小生成不同的类型。
1.2 非类型模板参数的使用场景
-
数组大小:常见的用途之一是用于确定数组的大小,如上面的示例所示。
-
编译时常量计算:可以在模板中传递编译时常量,进而根据不同的常量生成不同的类型或执行不同的逻辑。
-
类型安全:通过使用非类型模板参数,可以在编译时进行类型检查,提高类型安全性。
-
优化:通过在编译时使用常量,编译器可以进行优化,减少运行时的开销。
1.3 非类型模板参数的使用实例
如下代码,是我们使用非类型模板参数定义的一个模板参数,我们在template后面的<>中传递的是两个无符号整型值,即常量值。在下面的模板函数中的参数中我们也将我们上面的非类型模板参数带上。(至于那个非类型模板参数的值具体是多少就不需要我们自己来求了,我们只需要传递我们模板函数的参数,然后编译器自己会推导出来非类型模板参数的值的)
//使用非类型模板参数定义两个不同长度的字符串进行比较
template<size_t M,size_t N>
int Compare(const char(&p1)[M], const char(&p2)[N])
{return strcmp(p1, p2); //逐个字符进行ASCII码值进行比较,如果每个字符都相等就比较长度,如果长度不等就看前面哪个的字符ascii码值大
}int main()
{const char* str1 = { "aaaaaaa" };const char* str2 = { "hello" };//int ret = Compare(str1, str2); //这样传递参数是不可以的,因为我们在上面的模板函数中定义的参数是两个数组的引用,我们不能只传递数组的地址int ret = Compare("aaaaaaa", "hello"); //直接将字符数组的内容传递过去cout << ret << endl;return 0;
}
二、模板特例化
2.1 模板特例化的概念
我们编写单一模板的时候,使之传递任何类型的模板实参都是最合适的,都能够进行实例化,这并不现实。有时候因为一些特殊情况,通用模板类型对于一些特殊类型是不适用的:通用定义可能会编译失败或做的不正确。因此,有时候我们想使用其他特定的知识来编写更加高效的代码,于是我们针对那些特殊的类型来定义类或函数模板的特例化版本,因此模板特例化又分为函数模板特例化和类模板特例化。
2.2 函数模板特例化
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板 ;
2. 关键字template后面接一对空的尖括号< > ;
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型 ;
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
//定义通用模板函数
template<class T>
bool Less(T x, T y)
{return x < y;
}//模板函数特例化
template<>
bool Less<int*>(int* x, int* y)
{return *x < *y;
}
我们通过上面的具体代码,我们可以知道,函数模板特例化就是将我们的class T换下来了,换成了一个具体的数据类型,这个与我们的之前重载函数是不是有点像呢?只是将函数的参数修改一下。因此对于这种遇到特殊的数据类型通用函数模板无法实现的时候,我们可以直接将这个函数实现出来。
bool Less(Date* left, Date* right)
{return *left < *right;
}
该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化 时特别给出,因此函数模板不建议特化。
2.3 函数模板特例化规则
- 特例化的函数模板和原模板必须在相同的作用域中。
- 完全特例化必须提供完整的类型参数,而偏特例化仅仅是对某些特定条件进行部分特化。
- 如果没有偏特例化或完全特例化,C++ 编译器将选择通用模板进行实例化。
2.4 完全特例化和偏特例化
函数模板特例化又分为完全特例化和偏特例化。
1.完全特例化:
完全特例化是指为特定类型的模板参数提供一个完全不同的实现。当我们明确知道某个特定类型需要不同的实现时,就可以使用完全特例化。
template <typename T>
T add(T a, T b)
{return a + b; // 通用实现
}// 完全特例化:当 T 是 int 类型时,使用不同的实现
template <>
int add<int>(int a, int b)
{std::cout << "Specialized version for int!" << std::endl;return a + b;
}
2. 偏特例化
偏特例化是指我们为模板参数的某些特定条件(比如某些特定类型或类型组合)提供一个新的实现,而不是为整个类型提供一个完全不同的实现。偏特例化并不是为某一个特定类型提供一个实现,而是为一个类型的类别(如指针、引用等)提供实现。
template <typename T>
T add(T a, T b)
{return a + b; // 通用实现
}// 偏特例化:为指针类型提供一个实现
template <typename T>
T* add(T* a, T* b){std::cout << "Specialized version for pointers!" << std::endl;return *a + *b;
}
偏特例化通常用于以下几种情况:
- 类型分类:例如,我们希望为指针类型、引用类型或数组类型提供不同的实现。
- 条件判断:例如,当模板参数是某种类型时,采用不同的算法或逻辑。
3. 完全特例化与偏特例化的区别
- 完全特例化:是为特定类型提供一个完全不同的实现,通常是某个模板实例化后的一种完全不同的版本。
- 偏特例化:是根据模板参数的一部分(如类型类别或类型的组合)来提供一个不同的实现,通常用于处理某些特定类型的情况(例如指针、数组、引用等)。
2.5 类模板特例化
类模板特例化(Template Specialization)是C++中的一个特性,它允许你为某些特定类型的模板实例提供不同的实现。模板特例化有两种形式:完全特例化(Full Specialization)和偏特例化(Partial Specialization)。
类模板的特化步骤:
1. 必须要先有一个基础的类模板;
2. 关键字template后面接一对空的尖括号< > ;
3.类名后跟一对尖括号,尖括号中指定需要特化的类型 ;
4. 类中成员变量类型要与特化类型一致,成员方法中的参数类型也要与我们的特化类型保持一致。
//类模板特化
template <class T1,class T2>
class Date
{
public:Date(){cout << "Date<T1,T2>" << endl;}
private:T1 d1;T2 d2;
};//类模板的全特化
template <>
class Date<int,char>
{
public:Date(){cout << "Date<T1,int>" << endl;}
private:int d1;char d2;
};//类模板的偏特化
template <class T1>
class Date<T1,char>
{
public:Date(){cout << "Date<T1,char>" << endl;}
private:T1 d1;char d2;
};template <>
class Date<int, int>
{
public:Date(){cout << "Date<int,int>" << endl;}
private:int d1;int d2;
};//两个参数偏特化成引用类型
template <typename T1, typename T2>
class Date <T1&, T2&>
{
public:Date(const T1& d1, const T2& d2) //我们的这里的构造函数传递了参数,因此我们在实例化对象时也要传递参数: _d1(d1), _d2(d2){cout << "Data<T1&, T2&>" << endl;}private:const T1& _d1;const T2& _d2;
};template <>
class Date<int*, int*>
{
public:Date(){cout << "Date<int*int*>" << endl;}
private:int* d1;int* d2;
};
上述代码是我们对类模板进行完全特例化和偏特例化的各种版本,我们可以看到对于完全特例化,我们只需将class T去掉,然后加上我们相要的特定数据类型即可,但是偏特例化,我们有时候仅仅将我们想要的数据类型换上去还是不写的,有时候我们还得对构造函数进行修改(例如上面的偏特例化为引用类型)。
完全特例化与偏特例化的辨析
对于类模板是否是完全特例化还是偏特例化,我觉得不光光只是看template后面接的< >中有没有class定义的参数T,我们要看它是否给出所有具体的数据类型,如果给了所有的具体数据类型,我们再看template后面的< >中是否还有参数,如果没有参数,那么就是完全特例化;如果还有参数,那么就是偏特例化(我们给定的具体数据类型仅仅是对我们的模板参数进行进一步限制的)
三、模板的分离编译
3.1 分离编译
分离编译(Separate Compilation)是指将程序的各个模块或源代码文件独立编译,然后再将它们链接成一个完整的可执行程序的过程。分离编译的关键思想是将一个大的程序分成多个小的模块,每个模块单独编译并生成目标文件(通常是 .obj
或 .o
文件),最后通过链接器将这些目标文件链接成最终的可执行文件或库文件。
分离编译的优点:
-
提高编译效率:
- 对于大型项目,分离编译可以避免每次修改代码后都要重新编译整个程序。只需要重新编译发生变化的模块,节省了编译时间。
-
模块化管理:
- 代码可以被划分成独立的模块,便于代码的维护、更新和复用。不同模块之间的修改可以相互独立,减少了不同开发人员之间的耦合。
-
提高可维护性:
- 将程序划分为多个模块,每个模块可以独立测试和调试,降低了复杂性,便于开发团队进行协作开发。
-
避免重复编译:
- 在大型项目中,分离编译使得每个模块只在修改后重新编译,而不是每次都重新编译所有代码,减少了不必要的编译工作。
3.2 函数分离编译
我们在之前数据结构学习阶段的时候,我们模拟实现某种数据结构时,我们经常使用的是分离编译的形式,我们将函数的的声明与定义以及一些测试代码放在一个项目的不同文件中。我们将那些函数声明单独放在一个文件中时,我们在源文件(包含了那些有函数声明的文件头文件)中任意位置调用那些函数都不会出现未声明函数的的报错了。而且我们使用不同的文件来编写不同的代码,这样编写的代码看起来更加明朗而且清楚。
为什么函数可以进行分离编译呢?
函数可以进行分离编译,主要得益于函数接口的声明与实现的分离、编译器和链接器的协同工作。通过分离编译,程序员能够更高效地组织代码、减少编译时间、方便模块化开发,同时也为函数的重用和库的构建提供了便利。
3.3 模板的分离编译
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
// main.cpp
#include"a.h"
int main()
{Add(1, 2);Add(1.0, 2.0);return 0;
}
由上面的代码验证与分析,我们可以得出结论:模板是不可以进行分离编译的。通常,当我们调用一个函数时,编译器只要能够掌握函数的声明就可以了,因为有了函数的声明,那样编译器就知道这个函数是确实存在的,那么它就可以安心进行下面的操作了,但是调用一个模板却不同,我们为了生成一个实例化版本,编译器需要掌握函数模板或者类模板成员函数的定义。如果我们在一个文件中只有模板函数或模板类的声明,编译器调用这个文件时,找不到函数模板/类模板的定义是无法进行实例化的,因此链接时会发生报错。
如何解决?
1. 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。
2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用。
四、模板的总结
【优点】 1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
【缺陷】 1. 模板会导致代码膨胀问题,也会导致编译时间变长(我们对于一些特例化模板操作,可能会出现一些代码重复出现的情况);
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。