c++ 中的模板通过将类型参数化,可以提高代码的复用性。模板并不能减少代码量,只是从开发者的角度来看,代码量减少了,复用性提高了;从二进制文件的角度看,代码量没有减小。
1 函数模板
当求两个数的和时,数据的类型可能是 int、float 或者 double等,如果不使用模板的话,我们需要写下边的代码,每种数据类型都实现一个函数。这样当我们需要使用其它数据类型时,就需要再写一个函数。
#include <iostream>
#include <string>
int sum(int a, int b) {
std::cout << "int" << std::endl;
return a + b;
}
float sum(float a, float b) {
std::cout << "float" << std::endl;
return a + b;
}
double sum(double a, double b) {
std::cout << "double" << std::endl;
return a + b;
}
int main() {
std::cout << sum(1, 2) << std::endl;
std::cout << sum(1.1f, 2.2f) << std::endl;
std::cout << sum(1.1, 2.2) << std::endl;
return 0;
}
c++ 提供了模板,模板可以把类型参数化。针对上边的代码,在 c++ 中就可以使用模板函数来实现。代码如下所示,只需要定义一个模板函数就可以。
#include <iostream>
#include <string>
template <class T>
T sum(T a, T b) {
std::cout << "sum" << std::endl;
return a + b;
}
int main() {
std::cout << sum(1, 2) << std::endl; // 隐式调用, 类型为 int
std::cout << sum(1.1f, 2.2f) << std::endl; // 隐式转换,类型是 float
std::cout << sum(1.1, 2.2) << std::endl; // 隐式转换,类型是 double
std::cout << sum<int>(1, 2.2) << std::endl; // 显示转换,类型是 int, 如果不显式指定类型,编译器会出现二义性,到底是 int 还是 double
return 0;
}
模板函数的代码量减少了,但是编译之后的二进制文件并没有减少。使用 objdump -tT a.out 可以看到,其中生成了 3 个函数实例。将程序员的工作转化成了编译器的工作,如果是不使用模板,就需要开发者自己写那么多函数;使用模板的方式来定义函数,编译器会生成这些函数。
2 类模板
如下代码, Point_T 是一个模板类,表示一个点,其中包括点的横纵坐标 x 和 y,类型分别是 T1 和 T2。
#include <iostream>
template <class T1, class T2>
class Point_T {
public:
T1 x_;
T2 y_;
Point_T() : x_(0), y_(0) {
std::cout << "default constructor" << std::endl;
}
Point_T(T1 x, T2 y) : x_(x), y_(y) {
std::cout << "constructor" << std::endl;
}
Point_T<T1, T2>& operator=(const Point_T<T1, T2> &point) {
this->x_ = point.x_;
this->y_ = point.y_;
return *this;
}
// +运算符重载,需要声明为 friend
friend Point_T<T1, T2> operator +(Point_T<T1, T2> &point1, Point_T<T1, T2> &point2) {
Point_T<T1, T2> tmp;
tmp.x_ = point1.x_ + point2.x_;
tmp.y_ = point1.y_ + point2.y_;
return tmp;
}
// << 运算符重载,需要声明为 friend
friend std::ostream& operator<< (std::ostream &out, const Point_T<T1, T2>& point) {
out << "(" << point.x_ << ", " << point.y_ << ")" << std::endl;
return out;
};
};
int main() {
Point_T<int, int> int_point1(1, 2);
Point_T<int, int> int_point2(10, 20);
// 看到一些书上说了类模板必须指定参数类型
// 但是在自己的虚拟机上不指定,编译和运行也是可以的
// 在实际使用中后,还是现实指定类型比较规范
Point_T int_point3(10, 20);
Point_T<float, float> float_point1(1.1f, 2.2f);
Point_T<float, float> float_point2(10.10f, 20.20f);
Point_T<int, int> int_total_point;
Point_T<float, float> float_total_point;
int_total_point = int_point1 + int_point3;
float_total_point = float_point1 + float_point2;
std::cout << int_total_point << std::endl;
std::cout << float_total_point << std::endl;
return 0;
}
通过 objdump 也能看出来,代码中根据参数类型对类进行了实例化。
3 常见问题
3.1 模板列表中能不能有具体的数据类型
如下代码,模板列表中有一个类型是基本数据类型是 int,这种方式是合法的,编译运行都没问题。不过在开发中基本不这么使用,在实际使用中,模板列表都是抽象的类型,而不是具体的类型。
#include <iostream>
#include <string>
template <class T, int data>
class Test {
public:
void Do() {
std::cout << "data = " << data << std::endl;
}
};
int main() {
Test<int, 1> t1;
Test<int, 10> t2;
Test<int, 100> t3;
t1.Do();
t2.Do();
t3.Do();
return 0;
}
使用 objdump 可以看到,在代码段生成了 3 份 Do() 函数。
3.2 模板特化
3.2.1 函数模板特化
如下 IsEqual() 是一个模板函数,但是在参数类型是 char * 的时候,使用 t1 == t2 来判断是不对的。针对 char * 类型,可以将模板特化,也就是单独写一个判断字符串是否相等的函数。
函数模板的特化就是在函数上面写一个空的模板列表 template<>,然后函数的参数类型写成具体的数据类型就可以。
#include <iostream>
#include <string>
#include <cstring>
template <class T>
bool IsEqual(T t1, T t2) {
return t1 == t2;
}
// 模板特化
template <>
bool IsEqual(char *t1, char *t2) {
return strcmp(t1, t2) == 0;
}
int main() {
char str1[] = "hello";
char str2[] = "hello";
std::cout << IsEqual(10, 10) << std::endl;
std::cout << IsEqual(str1, str2) << std::endl;
return 0;
}
3.2.2 类模板的特化
类的特化和函数的特化类似,在累的声明前边声明一个空的模板列表,然后在类名后边声明具体的数据类型。
#include <iostream>
#include <string>
#include <cstring>
template <class T>
class Compare {
public:
bool IsEqual(T t1, T t2) {
return t1 == t2;
}
};
// 模板特化
template <>
class Compare<char *> {
public:
bool IsEqual(char* t1, char* t2) {
return strcmp(t1, t2) == 0;
}
};
int main() {
char str1[] = "hello";
char str2[] = "hello";
Compare<int> c1;
Compare<char *> c2;
std::cout << c1.IsEqual(10, 10) << std::endl;
std::cout << c2.IsEqual(str1, str2) << std::endl;
return 0;
}
3.3 可变参数模板
如下代码中 Thread 的构造函数中包含一个可变参数模板。
可变参数模板在模板列表中的格式: class... Args
可变参数模板在形参中的格式:Args && ... args
可变参数使用 std::forward 进行转发的格式:std::forward<Args>(args) ...
可变参数经常和 std::forward 结合使用,保证参数的左值或者右值属性不变。
计算可变参数的参数个数:sizeof...(arga)
#include <iostream>
#include <string>
#include <thread>
#include <memory>
class Thread {
public:
template <class Function, class ... Args>
Thread(Function &&f, Args...args) noexcept {
std::cout << "arg count " << sizeof...(args) << std::endl;
internal_ = std::make_shared<std::thread>(std::forward<Function>(f), std::forward<Args>(args)...);
}
virtual ~Thread() noexcept {
if (internal_->joinable()) {
internal_->join();
}
}
private:
std::shared_ptr<std::thread> internal_;
};
void thread_func(int a, int b) {
std::cout << "thread func, a = " << a << ", b = " << b << std::endl;
}
int main() {
Thread t(thread_func, 10, 20);
return 0;
}
使用函数重载和递归方式展开可变参数:
一个执行函数,一个终止递归的函数。
如下代码中,两个函数 printf1() 线程重载,其中一个函数只接收一个参数,一个函数接收一个确定的参数和一个可变的参数。那么在调用 printf1() 的时候,如果参数个数大于 1 个,那么就会调用第二个函数,如果参数个数是 1 个,那么就会调用第一个函数。
#include <iostream>
template<typename T0>
void printf1(T0 value) {
std::cout << "one arg: " << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << "packaged args: " << value << std::endl;
printf1(args...);
}
int main() {
printf1(1, 2, "123", 1.1);
return 0;
}
运行结果如下,可以看出来,前 3 个参数是第二个 printf1() 打印出来,最后一个参数是第一个 printf1() 打印出来。
如下代码,也是两个函数重载。其中一个函数没有入参,那么最后剩一个参数的时候也是调用第二个 sum,最后一次没有参数的时候调用第一个 sum。
#include <iostream>
// 基本情况:当没有参数时,返回0
int sum() {
std::cout << "sum()\n";
return 0;
}
// 递归情况:计算第一个参数加上剩余参数的总和
template<typename T, typename... Args>
T sum(T first, Args... args) {
std::cout << "package args, first = " << first << std::endl;
return first + sum(args...);
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << std::endl; // 输出 15
std::cout << sum(10, 20, 30) << std::endl; // 输出 60
std::cout << sum(2.5, 3.5, 4.5) << std::endl; // 输出 10.5
return 0;
}