好奇的探索者,理性的思考者,踏实的行动者。
Table of Contents:
所谓“泛型”,指的是算法只要实现一遍,就能适用于多种数据类型。优势在于能够减少重复代码的编写。本质的类型参数化。
泛型程序设计就是大量编写模板、使用模板的程序设计。泛型程序设计在 C++ 中的重要性和带来的好处不亚于面向对象的特性。
在C++中,数据的类型也可以通过参数来传递,在函数定义时可以不指明具体的数据类型,当发生函数调用时,编译器可以根据传入的实参自动推断数据类型。这就是类型的参数化。
在 C++ 中,模板分为函数模板和类模板两种。熟练的 C++ 程序员,在编写函数时都会考虑能否将其写成函数模板,编写类时都会考虑能否将其写成类模板,以便实现重用。
所谓函数模板,编写函数的时候用类型参数代替真正的参数,函数调用时再通过参数判断类型。
template<typename T>
void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
bool b1 = false, b2 = true;//编译器会根据参数自动判断类型 Swap(&b1, &b2);
typename关键字也可以使用class关键字替代,它们没有任何区别。C++ 早期对模板的支持并不严谨,没有引入新的关键字,而是用 class 来指明类型参数,但是 class 关键字本来已经用在类的定义中了,这样做显得不太友好,所以后来 C++ 又引入了一个新的关键字 typename,专门用来定义类型参数。不过至今仍然有很多代码在使用 class 关键字,包括 C++ 标准库、一些开源程序等。
一但声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句话说,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。
模板类声明:
//这里不能有分号
template<typename T1, typename T2>
class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public://获取x坐标
T1 getX() const; //设置x坐标
void setX(T1 x); //获取y坐标
T2 getY() const; //设置y坐标
void setY(T2 y);
private://x坐标
T1 m_x; //y坐标
T2 m_y; };
模板类方法实现:
//模板头
template<typename T1, typename T2> /*函数头*/ {
T1 Point<T1, T2>::getX() const
return m_x; }
注意:类名后面也要带上类型参数,只是不加typename关键字了。
使用类模板创建对象:
int, int> p1(10, 20);
Point<int, float> p2(10, 15.5);
Point<float, float> *p1 = new Point<float, float>(10.6, 109.3);
Point<char*, char*> *p = new Point<char*, char*>("东经180度", "北纬210度"); Point<
与函数模板不同的是,类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型。
计算机编程语言种类繁多,这些编程语言根据不同的标准可以分为不同的种类,根据“在定义变量时是否需要显式地指明数据类型”可以分为强类型语言和弱类型语言。
对于强类型的语言,变量的类型从始至终都是确定的、不变的,编译器在编译期间就能检测某个变量的操作是否正确,这样最终生成的程序中就不用再维护一套类型信息了,从而减少了内存的使用,加快了程序的运行。
不过这种说法也不是绝对的,有些特殊情况还是要等到运行阶段才能确定变量的类型信息。比如 C++ 中的多态,编译器在编译阶段会在对象内存模型中增加虚函数表、type_info 对象等辅助信息,以维护一个完整的继承链,等到程序运行后再执行一段代码才能确定调用哪个函数
对于弱类型的语言,变量的类型可以随时改变,赋予它什么类型的数据它就是什么类型,编译器在编译期间不好确定变量的类型,只有等到程序运行后、真的赋给变量一个值了,才能确定变量当前是什么类型,所以传统的编译对弱类型语言意义不大,因为即使编译了也有很多东西确定不下来。
我们将这种一边执行一边编译的语言称为解释型语言,而将传统的先编译后执行的语言称为编译型语言。
强类型语言较为严谨,在编译时就能发现很多错误,适合开发大型的、系统级的、工业级的项目;
而弱类型语言较为灵活,编码效率高,部署容易,学习成本低,在 Web 开发中大显身手。另外,强类型语言的 IDE 一般都比较强大,代码感知能力好,提示信息丰富;而弱类型语言一般都是在编辑器中直接书写代码。
C++ 模板也是被迫推出的,最直接的动力来源于对数据结构的封装。数据结构关注的是数据的存储,以及存储后如何进行增加、删除、修改和查询操作,但是数据结构中每份数据的类型无法提前预测。以链表为例,它的每个节点可以用来存储小数、整数、字符串等,也可以用来存储一名学生、教师、司机等,还可以直接存储二进制数据,这些都是可以的,没有任何限制。而 C++ 又是强类型的,数据的种类受到了严格的限制,这种矛盾是无法调和的,于是模板就诞生了。模板虽然不是 C++ 的首创,但是却在 C++ 中大放异彩,后来也被 Java、C# 等其他强类型语言采用。
//模板①:交换基本类型的值
template<class T> void Swap(T &a, T &b); //模板②:交换两个数组,注意:最后一个参数可以是非泛型的 template<typename T> void Swap(T a[], T b[], int len);
对于非模板函数,发生函数调用时会对实参的类型进行适当的转换,以适应形参的类型。这些转换包括:
* 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
* 派生类向基类的转换:也就是向上转型
* const 转换:也即将非 const 类型转换为 const 类型,例如将 char *
转换为 const char *
。
* 数组或函数指针转换:如数组名会转换为数组指针,函数名也会转换为函数指针。
* 用户自定的类型转换。
而对于函数模板,类型转换则受到了更多的限制,仅能进行const 转换和数组或函数指针转
函数模板的实参推断是指「在函数调用过程中根据实参的类型来寻找类型参数的具体类型」的过程,这在大部分情况下是奏效的,但是当函数参数少于类型参数时。
「为函数模板显式地指明实参」和「为类模板显式地指明实参」的形式是类似的,就是在函数名后面添加尖括号< >
,里面包含具体的类型。
int, int>(10); func<
上面我们提到,函数模板仅能进行「const 转换」和「数组或函数指针转换」两种形式的类型转换,但是当我们显式地指明类型参数的实参(具体类型)时,就可以使用正常的类型转换(非模板函数可以使用的类型转换)了。
例如对于下面的函数模板:
template<typename T> void func(T a, T b);
它的具体调用形式如下:
10, 23.5); //Error
func(float>(20, 93.7); //Correct func<
显示的指明模板的实参是个好习惯
struct X { typedef int foo; }; 123; };
struct Y { static int const foo =
// T::foo的可能是一个类型,也可能是一个变量,这个就存在歧义了
// 可以认为 int * x; 一个变量定义
// 也可以认为 123 * x; 一个乘法表达式
template<class T> void f_tmpl () { T::foo * x; }
c++中这种情况被称为dependent names。Dependent Names 是指那些在模板中的名称,其含义需要根据模板参数的实际类型来确定,编译器会在模板实例化阶段进行解析。
为了解决这个问题,C++中引入了 typename 关键字,用来告诉编译器 T::foo 是一个类型。在模板定义中,当涉及到依赖名称时,你可以使用 typename 来明确指定这个名称是一个类型。
修复后如下:
void f_tmpl() { typename T::foo * x; } template<class T>
template<typename T>
struct MyType {//c++中这叫做alias template
using iterator = ...;
...
};
template<typename T>// typename必须有
using MyTypeIterator = typename MyType<T>::iterator; int> pos; MyTypeIterator<
上面的注释说明了:typename MyType<T>::iterator
里的typename是必须的,因为这里的typename代表后面紧跟的是一个定义在类内的类型,否则,iterator会被当成一个静态变量或者枚举
C++ 没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。
但是现在我们希望改变这种“游戏规则”,让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显示具体化(Explicit Specialization)。
语法:1. template<>
2. 其他的类型参数都换成具体类型。
#include <iostream>
#include <string>
using namespace std;
typedef struct{
string name;
int age;
float score;
} STU;
//函数模板
template<class T> const T& Max(const T& a, const T& b);
//函数模板的显示具体化(针对STU类型的显示具体化)
template<> const STU& Max<STU>(const STU& a, const STU& b);
//重载<<
ostream & operator<<(ostream &out, const STU &stu);
int main(){10;
int a = 20;
int b =
cout<<Max(a, b)<<endl;"王明", 16, 95.5};
STU stu1 = { "徐亮", 17, 90.0};
STU stu2 = {
cout<<Max(stu1, stu2)<<endl;0;
return
}
template<class T> const T& Max(const T& a, const T& b){
return a > b ? a : b;
}//函数模板的显示具体化
template<> const STU& Max<STU>(const STU& a, const STU& b){
return a.score > b.score ? a : b;
}
ostream & operator<<(ostream &out, const STU &stu){" , "<<stu.age <<" , "<<stu.score;
out<<stu.name<<
return out; }
在 C++ 中,对于给定的函数名,可以有1.非模板函数、2.模板函数、3.显示具体化模板函数以及它们的重载版本
在调用函数时: 非模板函数 > 显示具体化 > 常规模板
#include <iostream>
using namespace std;//类模板
template<class T1, class T2> class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
T1 m_x;
T2 m_y;
};
//这里要带上模板头
template<class T1, class T2>
void Point<T1, T2>::display() const{"x="<<m_x<<", y="<<m_y<<endl;
cout<<
}
//类模板的显示具体化(针对字符串类型的显示具体化)
char*, char*>{
template<> class Point<
public:
Point(char *x, char *y): m_x(x), m_y(y){ }
public:
char *getX() const{ return m_x; }
void setX(char *x){ m_x = x; }
char *getY() const{ return m_y; }
void setY(char *y){ m_y = y; }
void display() const;
private://x坐标
char *m_x; //y坐标
char *m_y;
};//这里不能带模板头template<>
char*, char*>::display() const{
void Point<"x="<<m_x<<" | y="<<m_y<<endl;
cout<<
}
int main(){int, int>(10, 20) ) -> display();
( new Point<int, char*>(10, "东京180度") ) -> display();
( new Point<char*, char*>("东京180度", "北纬210度") ) -> display();
( new Point<0;
return }
需要定义两套类,一个模板的,一个具体类型的。
在上面的显式具体化例子中,我们为所有的类型参数都提供了实参,所以最后的模板头为空,也即template<>。另外 C++ 还允许只为一部分类型参数提供实参,这称为部分显式具体化。
部分显式具体化只能用于类模板,不能用于函数模板。
此种方法依然是重写一套类,只不过类中可以部分的是类型参数。
#include <iostream>
using namespace std;//类模板
template<class T1, class T2> class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
T1 m_x;
T2 m_y;
};//这里需要带上模板头
template<class T1, class T2>
void Point<T1, T2>::display() const{"x="<<m_x<<", y="<<m_y<<endl;
cout<<
}//类模板的部分显示具体化
char*, T2>{
template<typename T2> class Point<
public:
Point(char *x, T2 y): m_x(x), m_y(y){ }
public:
char *getX() const{ return m_x; }
void setX(char *x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private://x坐标
char *m_x; //y坐标
T2 m_y;
};//这里需要带上模板头
template<typename T2> char*, T2>::display() const{
void Point<"x="<<m_x<<" | y="<<m_y<<endl;
cout<< }
模板是一种泛型技术,目的是将数据的类型参数化,以增强 C++ 语言(强类型语言)的灵活性。C++ 对模板的支持非常自由,模板中除了可以包含类型参数,还可以包含非类型参数,例如:
template<typename T, int N> class Demo{ };
template<class T, int N> void func(T (&arr)[N]);
template<typename T> void Swap(T a[], T b[], int len);
多出来的形参 len 给编码带来了不便,我们可以借助模板中的非类型参数将它消除。
template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]){
T temp;0; i<N; i++){
for(int i=
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}//调用,编译器会自动推断类型为int,长度为5
5] = { 1, 2, 3, 4, 5 };
int a[5] = { 10, 20, 30, 40, 50 };
int b[ Swap(a, b);
T (&a)[N]
表明 a 是一个引用,它引用的数据的类型是T [N]
,也即一个数组;
分析一个引用和分析一个指针的方法类似,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析
以动态数组为例讲解
template<typename T, int N>
Array<T, N>::Array(){
m_p = new T[N];
m_capacity = m_length = N; }
Array 是一个类模板,它有一个类型参数T和一个非类型参数N,T
指明了数组元素的类型,N
指明了数组长度。
模板(Templet)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一个模板。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数或类的过程叫做模板的实例化(Instantiate),相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例(Instantiation)。
编译器会帮助我们自动地生成这些代码。从这个角度理解,模板也可以看做是编译器的一组指令,它命令编译器生成我们想要的代码。
模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。
另外需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。
将类的声明和类的实现分别放到头文件和源文件中。类的声明已经包含了所有成员变量的定义和所有成员函数的声明(也可以是 inline 形式的定义),这样就知道如何创建对象了,也知道如何调用成员函数了,只是还不能将函数调用与函数实现对应起来,但是这又有什么关系呢,反正链接器可以帮助我们完成这项工作。
不管是函数还是类,声明和定义(实现)的分离其实是一回事,都是将函数定义放到其他文件中,最终要解决的问题也只有一个,就是把函数调用和函数定义对应起来(找到函数定义的地址,并填充到函数调用处),而保证完成这项工作的就是链接器。
基于传统的编程思维,初学者往往也会将模板(函数模板和类模板)的声明和定义分散到不同的文件中,以期达到「模块化编程」的目的。但事实证明这种做法是不对的,程序员惯用的做法是将模板的声明和定义都放到头文件中。
模板并不是真正的函数或类,它仅仅是用来生成函数或类的一张“图纸”,在这个生成过程中有三点需要明确:
* 模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码;
* 模板的实例化是由编译器完成的,而不是由链接器完成的;
* 在实例化过程中需要知道模板的所有细节,包含声明和定义(如果只包含声明的话,也即只包含头文件的话,由于没有定义而不会生成所需的方法)
前面讲到的模板的实例化是在调用函数或者创建对象时由编译器自动完成的,因此称为隐式实例化。
通过代码明确地告诉编译器需要针对哪个类型进行实例化,这称为显式实例化。
编译器在实例化的过程中需要知道模板的所有细节:对于函数模板,也就是函数定义;对于类模板,需要同时知道类声明和类定义。
我们必须将显式实例化的代码放在包含了模板定义的源文件中,这样编译器在编译时会在当前的.o
w文件中生成实例化后的代码
显式实例化的一个好处是:可以将模板的声明和定义(实现)分散到不同的文件中了。
// //显式实例化定义 template void Swap(double &a, double &b);
这条语言由两部分组成,前边是一个template关键字(后面不带< >),后面是一个普通的函数原型。
显式实例化也包括声明和定义,定义要放在模板定义(实现)所在的源文件,声明要放在模板声明所在的头文件(当然也可以不写)。
类模板的显式实例化和函数模板类似。
template class Point<char*, char*>;
相应地,它的声明形式为:
extern template class Point<char*, char*>;
不管是声明还是定义,都要带上class关键字,以表明这是针对类模板的。
另外需要注意的是,显式实例化一个类模板时,会一次性实例化该类的所有成员,包括成员变量和成员函数。
1. C++ 支持显式实例化的目的是为模块化编程提供一种解决方案,这种方案虽然有效,但是也有明显的缺陷:程序员必须要在模板的定义文件(实现文件)中对所有使用到的类型进行实例化。这就意味着,每次更改了模板使用文件(调用函数模板的文件,或者通过类模板创建对象的文件),也要相应地更改模板定义文件,以增加对新类型的实例化,或者删除无用类型的实例化。
2. 而对于库的开发者来说,他不能提前假设用户会使用哪些类型,所以根本就无法使用显式实例化,只能将模板的声明和定义(实现)全部放到头文件中;C++标准库几乎都是用模板来实现的,这些模板的代码也都位于头文件中。
总起来说,如果我们开发的模板只有我们自己使用,那也可以勉强使用显式实例化;如果希望让其他人使用(例如库、组件等),那只能将模板的声明和定义都放到头文件中了。
类模板和类模板之间、类模板和类之间可以互相继承。它们之间的派生关系有以下四种情况。
派生的类模板实例化的时候,编译器会自动生成其基类模板的模板类和派生类模板的模板类。
模板类: 从类模板中实例化后的类
派生的类模板实例化的时候,编译器会自动生成其基类的模板类和派生类的模板类。
template <class T>
class A{ T v1; int n; };int> { double v; };
class B: public A <0; } int main() { B obj1; return
比如全局的输入输出重载运算符做类的有元
最终生成的只是具体的模板函数
#include <iostream>
using namespace std;
template <class T>
class A
{
private:
static int count;
public:
A() { count ++; }
~A() { count -- ; };
A(A &) { count ++ ; }
static void PrintCount() { cout << count << endl; }
};
int>::count = 0; //初始化,对静态成员变量在类外部加以声明是必需的
template<> int A<double>::count = 0; //初始化
template<> int A<
int main()
{int> ia;
A<double> da;
A<
ia.PrintCount();
da.PrintCount();0;
return }
T
时,T::value
中的 T
就是一个依赖名称,因为它取决于实际使用模板时的类型。std::vector
中的 std
和 vector
就是非依赖名称,它们不会因为模板参数而改变。模板名称决议遵循两阶段查找的原则,其中包括两个阶段:
- 第一阶段(First Phase): 在模板定义的上下文中,编译器会查找并解析所有的非依赖名称,以确保模板的语法正确。在这个阶段,编译器只考虑非依赖名称。
- 第二阶段(Second Phase): 在模板实例化的上下文中,编译器会查找并解析所有的依赖名称,以确定实际使用模板时的含义。在这个阶段,编译器会考虑依赖名称,确保它们在实例化时具有正确的语义。
例子:
template <typename T>
class MyContainer {
public:
void print(T value) {
std::cout << value << std::endl;
}
};
int main() {
int> container;
MyContainer<42);
container.print(return 0;
}
MyContainer
: 在模板定义时已知,与模板参数无关,是一个非依赖名称。print
: 在模板定义时已知,与模板参数无关,是一个非依赖名称。std::cout
: 不依赖于模板参数,是一个非依赖名称。std::endl
: 不依赖于模板参数,是一个非依赖名称。T
: 依赖于模板参数,根据实际使用的模板参数来确定其类型,是一个依赖名称。模板定义的作用域(scope of the template definition)和模板实例化的作用域(scope of the template instantiation)
在 C++ 中,模板是一种通用的代码生成机制,允许您编写一次代码,然后可以通过提供不同的类型或值参数来生成多个具体版本。在进行模板名称决议时,涉及两个主要的作用域:模板定义的作用域和模板实例化的作用域。
1. 情况 1:scope of the template definition(模板定义的作用域): 在模板定义的作用域内,当模板的名称与非依赖名称(不依赖于模板参数的名称)发生冲突时,会使用定义模板的作用域进行名称决议。
2. 情况 2:scope of the template instantiation(模板实例化的作用域): 在模板实例化的作用域内,当模板的名称与依赖名称(依赖于模板参数的名称)发生冲突时,会使用实例化模板的作用域进行名称决议。
这种规则称为 "Two-Phase Lookup",即两阶段查找。在 C++ 中,模板名称的决议是分两个阶段进行的,以支持模板的类型独立性和代码重用。
这两个阶段的查找是为了确保代码在模板定义时具有良好的语法,以及在实例化时可以适应不同的上下文。您正确地指出,在这种情况下,非依赖名称和依赖名称的决议方式是不同的,这是为了保证模板的正确性和灵活性。
总的来说,这个名称决议的规则确保了模板代码可以在不同上下文中正确运行,同时也体现了 C++ 中模板的高度通用性和复杂性。