一、泛型编程
举个例子:假如我们有两个int类型数据,要对它们进行相加,还有两个double类型数据,要对它们进行相加,首先我们想到的是利用C++函数重载来写,可能是这样的。
int add(const int &a, const int &b)
{
return a + b;
}
double add(const double &a, const double &b)
{
return a + b;
}
使用函数重载可以解决这个问题,但存在不好的地方:
假如我们新增一个需求,有两个float类型数据,要对它们进行相加,这时我们又要重载一份对于float类型数据的add函数重载。使得代码重复性高,复用率低,过于冗余。
代码可维护性低,一个地方出错,对于重载函数的不同数据类型的版本,都得进行修改。
泛型编程:编写与数据类型无关的通用代码,提高代码复用率,可维护性,是提高代码复用性的一种手段。
模板是泛型编程的基础,C++模板有两种:函数模板和类模板
二、函数模板
函数模板代表了一个函数家族,该函数模板与数据类型无关。程序在编译阶段,编译器会根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数,程序在运行的过程中,使用到函数模板的地方,就会去调用特定版本的模板函数。
注意:函数模板不是一个函数,它不是具体要调用的某一个函数,而是一个模板。程序在编译阶段,编译器会根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数。
函数模板语法
template<typename T1, typename T2, ..., typename Tn>
返回值类型 函数名(参数列表)
{
//代码块
}
template:定义模板的关键字,后面跟的是尖括号 < >
typename:定义模板参数的关键字,也可以用 class关键字替代
T1, T2, ..., Tn:表示某种数据类型的形参
我们用函数模板来重写以上两个数进行相加的代码
#include <iostream>
using namespace std;
template<typename T>
T add(const T &a, const T &b)
{
return a + b;
}
int main(int argc, char *argv[])
{
int i1 = 1;
int i2 = 2;
add(i1, i2);
double d1 = 1.1;
double d2 = 2.2;
add(d1, d2);
float f1 = 1.1;
float f2 = 2.2;
add(f1, f2);
return 0;
}
函数模板原理
C++的程序编译有预编译,编译,汇编,链接四个过程。程序在编译阶段,编译器会根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数,程序在运行的过程中,使用到函数模板的地方,就会去调用特定版本的模板函数。
![](https://img-blog.csdnimg.cn/img_convert/9afd820bf20cf19acad3e0785f7f8ef4.png)
我们在C++Insights验证一下,程序在编译阶段,编译器会根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数。如下:
#include <iostream>
using namespace std;
template<typename T>
T add(const T & a, const T & b)
{
return operator+(a, b);
}
/* First instantiated from: insights.cpp:14 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int add<int>(const int & a, const int & b)
{
return a + b;
}
#endif
/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
double add<double>(const double & a, const double & b)
{
return a + b;
}
#endif
/* First instantiated from: insights.cpp:22 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
float add<float>(const float & a, const float & b)
{
return a + b;
}
#endif
int main(int argc, char ** argv)
{
int i1 = 1;
int i2 = 2;
add(i1, i2);
double d1 = 1.1000000000000001;
double d2 = 2.2000000000000002;
add(d1, d2);
float f1 = static_cast<float>(1.1000000000000001);
float f2 = static_cast<float>(2.2000000000000002);
add(f1, f2);
return 0;
}
函数模板实例化
程序在编译阶段,编译器根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数的过程,叫做函数模板实例化。
隐式实例化:让编译器根据实参的数据类型推演模板函数参数的实际类型
#include <iostream>
using namespace std;
template<typename T>
T add(const T &a, const T &b)
{
return a + b;
}
int main(int argc, char *argv[])
{
int i1 = 1;
int i2 = 2;
add(i1, i2); //编译器推演出模板函数的参数类型为int
double d1 = 1.1;
double d2 = 2.2;
add(d1, d2); //编译器推演出模板函数的参数类型为double
//add(i1, d1); //错误:i1为int, d1为double,编译器无法推演出模板函数
//参数的数据类型,也不允许将i1转换成double
return 0;
}
不是所有用到函数模板的地方,编译器都能推演出模板函数参数的实际类型,这时必须显式实例化
#include <iostream>
using namespace std;
template<typename T>
T* get_pointer()
{
return new T;
}
int main(int argc, char *argv[])
{
//int *p1 = get_p(10); //错误:不能根据返回值类型来推演函数模板的参数类型,编译器推演不出实际的参数类型
int *p2 = get_pointer<int>();
delete p2;
p2 = NULL;
return 0;
}
显式实例化:在函数名后用<>,在<>里面指定模板函数参数的实际类型
#include <iostream>
using namespace std;
template<typename T>
T add(const T &a, const T &b)
{
return a + b;
}
int main(int argc, char *argv[])
{
int i1 = 1;
int i2 = 2;
add(i1, i2); //编译器推演出模板函数的参数类型为int
double d1 = 1.1;
double d2 = 2.2;
add(d1, d2); //编译器推演出模板函数的参数类型为double
add(i1, (int)d1); //正确:d1强制类型转换成int
add<int>(i1, d1); //正确:显式说明模板函数的参数类型为int
return 0;
}
同名模板函数与同名函数
当程序中有同名,同参数类型的函数时,没有显式指定用模板函数的前提下,会调用同名函数。而且编译器也不会生成跟这个同名函数相同参数类型的模板函数。
#include <iostream>
using namespace std;
template<typename T>
T add(const T &a, const T &b)
{
return a + b;
}
int add(const int &a, const int &b)
{
return a + b;
}
int main(int argc, char *argv[])
{
int i1 = 1;
int i2 = 2;
add(i1, i2); //调用同名函数
add<int>(i1, i2); //调用模板函数,编译器根据这个调用才生成的特定版本模板函数
return 0;
}
模板函数跟普通函数的区别:普通函数可以进行自动类型转换,函数模板隐式实例化时不允许自动转换类型。
#include <iostream>
using namespace std;
template<typename T>
T add(const T &a, const T &b)
{
return a + b;
}
int main(int argc, char *argv[])
{
int i1 = 1;
int i2 = 2;
add(i1, i2); //编译器推演出模板函数的参数类型为int
double d1 = 1.1;
double d2 = 2.2;
add(d1, d2); //编译器推演出模板函数的参数类型为double
//add(i1, d1); //错误:i1为int, d1为double,编译器无法推演出模板函数
//参数的数据类型,也不允许将i1转换成double
return 0;
}
多参数函数模板
#include <iostream>
using namespace std;
template<typename T1, typename T2>
void print_array(const T1 &index, const T2 &val)
{
cout << "index = " << index << ", val = " << val << endl;
}
int main(int argc, char *argv[])
{
int arr_i[3] = {1, 2, 3};
for (int i = 0; i < 3; i++)
print_array(i, arr_i[i]); // index 是 int 类型, val 是 int 类型
double arr_d[3] = {1.1, 2.2, 3.3};
for (int i = 0; i < 3; i++)
print_array(i, arr_d[i]); // index 是 int 类型,val 是 double 类型
return 0;
}
三、类模板
类模板代表了一个类家族,该类模板与数据类型无关。程序在编译阶段,编译器会根据指定的模板数据类型生成特定版本的模板类,程序在运行的过程中,使用到类模板的地方,就会去调用特定版本的模板类中相对应的函数。
注意:类模板不是一个类,它不是具体能够实例化的某一个类,而是一个模板。程序在编译阶段,编译器会根据指定的模板数据类型生成特定版本的模板类,程序在运行的过程中,使用到类模板的地方,就会去调用特定版本的模板类中相对应的函数。
类模板语法
template<typename T1, typename T2, ..., typename Tn>
class 类名
{
//代码块
}
template:定义模板的关键字,后面跟的是尖括号 < >
typename:定义模板参数的关键字,也可以用 class关键字替代
T1, T2, ..., Tn:表示某种数据类型的形参
我们用类模板写一个具有x,y坐标的点类
#include <iostream>
#include <assert.h>
using namespace std;
template<typename T>
class Point
{
public:
Point(const T x, const T y)
{
m_x = x;
m_y = y;
}
void print() const
{
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
private:
T m_x;
T m_y;
};
int main(int argc, char *argv[])
{
Point<int> point_i(1, 2);
point_i.print();
Point<double> point_d(1.1, 2.2);
point_d.print();
}
类模板原理
C++的程序编译有预编译,编译,汇编,链接四个过程。程序在编译阶段,编译器会根据指定的模板数据类型生成特定版本的模板类,程序在运行的过程中,使用到类模板的地方,就会去调用特定版本的模板类中相对应的函数。
![](https://img-blog.csdnimg.cn/img_convert/c0ac75a481f868312bd3e6e781be6eaf.png)
我们在C++Insights验证一下,程序在编译阶段,编译器会根据指定的模板数据类型生成特定版本的模板类。如下:
#include <iostream>
#include <assert.h>
using namespace std;
template<typename T>
class Point
{
public:
inline Point(const T x, const T y)
{
this->m_x = x;
this->m_y = y;
}
inline void print() const
{
operator<<(operator<<(operator<<(operator<<(operator<<(std::operator<<(std::cout, "("), this->m_x), ", "), this->m_y), ")"), endl);
}
private:
T m_x;
T m_y;
};
/* First instantiated from: insights.cpp:25 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Point<int>
{
public:
inline Point(const int x, const int y)
{
this->m_x = x;
this->m_y = y;
}
inline void print() const
{
std::operator<<(std::operator<<(std::operator<<(std::cout, "(").operator<<(this->m_x), ", ").operator<<(this->m_y), ")").operator<<(std::endl);
}
private:
int m_x;
int m_y;
public:
};
#endif
/* First instantiated from: insights.cpp:28 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Point<double>
{
public:
inline Point(const double x, const double y)
{
this->m_x = x;
this->m_y = y;
}
inline void print() const
{
std::operator<<(std::operator<<(std::operator<<(std::cout, "(").operator<<(this->m_x), ", ").operator<<(this->m_y), ")").operator<<(std::endl);
}
private:
double m_x;
double m_y;
public:
};
#endif
int main(int argc, char ** argv)
{
Point<int> point_i = Point<int>(1, 2);
point_i.print();
Point<double> point_d = Point<double>(1.1000000000000001, 2.2000000000000002);
point_d.print();
return 0;
}
类模板实例化
类模板必须在类名后用<>,在<>里面显式的指定类模板的实际参数类型进行实例化,编译器是无法跟部分函数模板那样,能够用实参去推演模板函数的数据类型的。
int main(int argc, char *argv[])
{
//Point point_i(1, 2); //错误:类模板必须显式指定参数数据类型
Point<int> point_i(1, 2); //正确
point_i.print();
}
同名的类模板与同名的类是不能共存的,编译器会报错。
类外定义模板类的相关成员函数
#include <iostream>
#include <assert.h>
using namespace std;
template<typename T>
class Point
{
public:
Point(const T x, const T y)
{
m_x = x;
m_y = y;
}
void print() const;
private:
T m_x;
T m_y;
};
template<typename T>
void Point<T>::print() const //是Point
{
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
int main(int argc, char *argv[])
{
Point<int> point_i(1, 2);
point_i.print();
Point<double> point_d(1.1, 2.2);
point_d.print();
}
注意:不是void Point::print() const,因为Point不是类名,Point<T>才是真正的类名。而且每个类外定义的函数前一行都要声明template<typename T>
类模板应用举例:实现一个支持多维的点模板类
point.cpp
#include <iostream>
#include <assert.h>
using namespace std;
template<typename T, int dim>
class Point
{
public:
Point(const T coords[])
{
for (int i = 0; i < dim; i++)
m_coords[i] = coords[i];
}
T& operator[](const int index)
{
assert(index >= 0 && index < dim);
return m_coords[index];
}
const T& operator[](const int index) const
{
assert(index >= 0 && index < dim);
return m_coords[index];
}
private:
T m_coords[dim];
};
template<typename T, int dim>
ostream& operator<<(ostream &cout, const Point<T, dim> &point)
{
cout << "(";
for (int i = 0; i < dim-1; i++)
cout << point[i] << ", ";
cout << point[dim-1];
cout << ")";
}
int main(int argc, char *argv[])
{
int coords_i[] ={1, 2, 3};
Point<int, 3> point_i(coords_i);
cout << point_i << endl;
double coords_d[] = {1.1, 2.2};
Point<double, 2> point_d(coords_d);
cout << point_d << endl;
}
![](https://img-blog.csdnimg.cn/img_convert/d57a5d854139734061ac09e01b28cc21.png)
四、模板形参类型
模板形参类型分为:类型形参和非类型形参
类型形参:出现在模板参数列表中,跟在class或者typename之后的参数类型名称。
非类型形参:用一个常量作为函数模板或类模板的一个参数,在函数模板或类模板中可将该参数当成常量来使用
formal_param_template.cpp
#include <iostream>
using namespace std;
template<typename T = int, int dim = 2> //dim是非类型形参
class Point
{
public:
Point(const T coords[])
{
for (int i = 0; i < dim; i++) //dim是非类型形参,在函数模板或类模板中可将该参数当成常量来使用
m_coords[i] = coords[i];
}
T& operator[](const int index)
{
return m_coords[index];
}
const T& operator[](const int index) const
{
return m_coords[index];
}
private:
T m_coords[dim]; //dim是非类型形参,在函数模板或类模板中可将该参数当成常量来使用
};
template<typename T, int dim>
ostream& operator<<(ostream &cout, const Point<T, dim> &point)
{
cout << "(";
for (int i = 0; i < dim-1; i++)
cout << point[i] << ", ";
cout << point[dim-1];
cout << ")";
}
int main(int argc, char *argv[])
{
int coords_i[] ={1, 2};
Point<> point_i(coords_i); //默认 int, 2 维
cout << point_i << endl;
double coords_d[] = {1.1, 2.2};
Point<double> point_d(coords_d); //默认 2 维
cout << point_d << endl;
}
![](https://img-blog.csdnimg.cn/img_convert/719a52655c1ac396c7408cbbea797b30.png)
五、模板特化
一般来说使用模板可以实现一些与数据类型无关的代码,但对于某些特殊的数据类型可能会得到错误的结果,这时就要对模板进行特化处理。即:在原有函数模板或类模板的基础上,对特殊的数据类型进行重写,覆盖掉模板的实现方式。
模板特化分为函数模板特化和类模板特化。
函数模板特化
func_template_specialzation.cpp
#include <iostream>
using namespace std;
template<typename T>
bool equal(const T& a, const T& b)
{
return a == b;
}
int main()
{
char arr1[] = "abc";
char arr2[] = "abc";
bool r = equal(arr1, arr2);//false
cout << "r = " << r << endl; //r = 0
const char *p1 = "abc";
const char *p2 = "abc";
r = equal(p1, p2); //true
cout << "r = " << r << endl; //r = 1
}
![](https://img-blog.csdnimg.cn/img_convert/7941b70d1394a3db0477ca6be8e5ba09.png)
同一字符串比较的结果却是不相同的,因为指针p1,p2都指向常量区中 "abc" 存储的内存地址的起始地址,所以 p1 等于 p2 ,所以打印:r = 1。而 arr1 数组和 arr2 数组是在栈中创建的两块不相同的空间,也就是说数组 arr1 的起始地址和数组 arr2 的起始地址是不一样的,所以打印:r = 0,这就不符合我们想要的预期的效果,这时就要函数模板特化处理。
func_template_specialzation.cpp
#include <iostream>
#include <string.h>
using namespace std;
template<typename T>
bool is_equal(const T& a, const T& b)
{
cout << "is_equal template" << endl;
return a == b;
}
//方法一(针对字符串类型特化处理)
template< >
bool is_equal<const char*>(const char* const &a, const char* const &b)
{
cout << "is_equal template specialzation" << endl;
return strcmp(a, b) == 0;
}
//方法二(有参数类型相同的同名函数,编译器会优先调用参数类型相同的同名函数,不会调用模板函数)
bool is_equal(const char *p1, const char *p2)
{
cout << "is_equal const char *p1" << endl;
return strcmp(p1, p2) == 0;
}
bool is_equal(char *p1, char *p2)
{
cout << "is_equal char *p1" << endl;
return strcmp(p1, p2) == 0;
}
int main()
{
bool r = false;
char arr1[] = "abc";
char arr2[] = "abc";
r = is_equal<const char*>(arr1, arr2);//调用方法一中 is_equal 这个模板函数
cout << "r = " << r << endl; //r = 1
r = is_equal(arr1, arr2);//调用方法二中 is_equal(char *p1, char *p2)
cout << "r = " << r << endl; //r = 1
const char *p1 = "abc";
const char *p2 = "abc";
r = is_equal<const char*>(p1, p2);//调用方法一中 is_equal 这个模板函数
cout << "r = " << r << endl; //r = 1
r = is_equal(p1, p2); //调用方法二中 is_equal(const char *p1, const char *p2)
cout << "r = " << r << endl; //r = 1
}
![](https://img-blog.csdnimg.cn/img_convert/ee17b1f25765f6b1bb8e4c22bafc8c7c.png)
类模板特化
类模板特化分为全特化和偏特化
全特化:将模板参数列表中所有的参数都确定化
class_template_specialzation.cpp
#include <iostream>
#include <assert.h>
using namespace std;
template<typename T1, typename T2>
class People
{
public:
People(const T1 &attr1, const T2 &attr2):m_attr1(attr1), m_attr2(attr2)
{
cout << "People(const T1 &attr1, const T2 &attr2) template" << endl;
cout << "m_attr1 = " << m_attr1 << endl;
cout << "m_attr2 = " << m_attr2 << endl;
}
T1 m_attr1;
T2 m_attr2;
};
template< >
class People<string, int> //全特化 T1 = string,T2 = int
{
public:
People(const string &attr1, const int &attr2):m_attr1(attr1), m_attr2(attr2)
{
cout << "People(const string &attr1, const int &attr2) 全特化" << endl;
cout << "m_attr1 = " << m_attr1 << endl;
cout << "m_attr2 = " << m_attr2 << endl;
}
string m_attr1; //attr1
int m_attr2; //attr2
};
int main(int argc, char *argv[])
{
string name = "小明";
int age = 12;
People<string, int>(name, age); //调用全特化版本
People<int, string>(age, name); //调用模板类
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/9d94c9df0c2f1641ade99ad8d596772f.png)
偏特化:将模板参数列表中的部分参数确定化
class_template_specialzation.cpp
#include <iostream>
#include <assert.h>
using namespace std;
template<typename T1, typename T2>
class People
{
public:
People(const T1 &attr1, const T2 &attr2):m_attr1(attr1), m_attr2(attr2)
{
cout << "People(const T1 &attr1, const T2 &attr2) template" << endl;
cout << "m_attr1 = " << m_attr1 << endl;
cout << "m_attr2 = " << m_attr2 << endl;
}
T1 m_attr1;
T2 m_attr2;
};
template<typename T1>
class People<T1, int> //偏特化 T2 = int
{
public:
People(const T1 &attr1, const int &attr2):m_attr1(attr1), m_attr2(attr2)
{
cout << "People(const T1 &attr1, const int &attr2) 偏特化" << endl;
cout << "m_attr1 = " << m_attr1 << endl;
cout << "m_attr2 = " << m_attr2 << endl;
}
T1 m_attr1; //attr1
int m_attr2; //attr2
};
int main(int argc, char *argv[])
{
string name = "小明";
int age = 12;
int sex = 0; //0:女,1:男
People<string, int>(name, age); //调用偏特化版本
People<int, int>(sex, age); //调用偏特化版本
People<int, string>(age, name); //调用模板类
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/b07d8cec13c743e5e3fa25913281c1ec.png)
六、模板函数,类模板的成员函数声明,定义分离,导致的编译链接问题
add_template.h
#ifndef _ADD_TEMPLATE_h_
#define _ADD_TEMPLATE_H_
template<typename T>
T add(const T &a, const T &b); //模板函数声明
#endif /*_ADD_TEMPLATE_H_*/
add_template.cpp
#include "add_template.h"
template<typename T>
T add(const T &a, const T &b) //模板函数定义
{
return a + b;
}
main.cpp
#include <iostream>
#include "add_template.h"
using namespace std;
int main(int argc, char *argv[])
{
int a = add(1, 2);
cout << a << endl;
return 0;
}
g++ main.cpp -o main //链接的过程中出错
![](https://img-blog.csdnimg.cn/img_convert/ea73ac57758cf0d2517586ba9713e3ac.png)
解决方法:模板函数,类模板的成员函数声明,定义都放在.h文件中
add_template.h
#ifndef _ADD_TEMPLATE_h_
#define _ADD_TEMPLATE_H_
template<typename T>
T add(const T &a, const T &b); //模板函数声明
template<typename T>
T add(const T &a, const T &b) //模板函数定义
{
return a + b;
}
#endif /*_ADD_TEMPLATE_H_*/
main.cpp
#include <iostream>
#include "add_template.h"
using namespace std;
int main(int argc, char *argv[])
{
int a = add(1, 2);
cout << a << endl;
return 0;
}
g++ main.cpp -o main //成功生成可执行文件 main
![](https://img-blog.csdnimg.cn/img_convert/2bc277034fd49371202f97f7cd0e869f.png)
七、总结
主要内容
C++模板分为函数模板和类模板
函数模板代表了一个函数家族,该函数模板与数据类型无关。程序在编译阶段,编译器会根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数,程序在运行的过程中,使用到函数模板的地方,就会去调用特定版本的模板函数。
类模板代表了一个类家族,该类模板与数据类型无关。程序在编译阶段,编译器会根据指定的模板数据类型生成特定版本的模板类,程序在运行的过程中,使用到类模板的地方,就会去调用特定版本的模板类中相对应的函数。
优缺点
优点:使用模板可编写与数据类型无关的通用代码,提高代码复用率,可维护性。C++标准模板库(STL)主要就是运用了C++模板技术来实现。
缺点:
模板会导致代码膨胀问题,导致编译时间变长
当模板编译出错时,错误信息凌乱,不容易定位错误出现的地方
模板不支持分离编译,对想屏蔽不让客户看到的实现代码不友好