[c++] 模板

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;
}

  • 13
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值