【c/c++】模板和泛型编程

深藏阁楼爱情的钟 2023-10-16 23:06 72阅读 0赞

说明

面向对象编程和泛型编程都能用来处理在编写程序时不知道类型的情况。

面向对象能处理在运行之前都不知道类型的情况;而泛型编程中在编译时就能够确定类型。

模板是c++中泛型编程的基础。

函数模板

下面是函数模板的一个例子:

  1. #include <iostream>
  2. using std::cout;
  3. using std::endl;
  4. //模板定义:
  5. //关键字 <模板参数列表> 函数体
  6. template <typename T>
  7. int compare (const T &v1, const T &v2) {
  8. if (v1 < v2)
  9. return -1;
  10. if (v1 > v2)
  11. return 1;
  12. else
  13. return 0;
  14. }
  15. int main()
  16. {
  17. cout << compare(1.0, 2.3) << endl;//T是double类型
  18. cout << compare(10, 3) << endl;//T是int类型
  19. return 0;
  20. }

模板参数可以有两种类型:

1) 类型参数。如上例中的typename T,它用关键词typename也或者class来标记,且每个参数都要标记。类型参数就可以看成基本类型或者自定义类型,它们作为函数的返回值,参数,局部变量等等。

2) 非类型参数。它不使用关键字typename或者class,而是一个具体的类型名。

一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或左值引用。绑定到非类型整型参数的实参必须是一个常量表达式;绑定到指针或者引用非类型参数的实参必须具有静态的生存期。

下面是一个例子:

  1. //非类型参数模板定义
  2. template <unsigned N, unsigned M>
  3. int compare(const char(&p1)[N], const char(&p2)[M]) {
  4. return strcmp(p1, p2);
  5. }
  6. int main()
  7. {
  8. cout << compare("hello", "world") << endl;
  9. return 0;
  10. }

上例中,编译器实例化后的compare形式如下:

  1. int compare(const char(&p1)[6], const char(&p2)[6])

这里使用了数组的const引用,这样保证了函数参数可以是不能拷贝的类型,且性能上也有所提高。需要注意的就是数组引用的写法,下面是一个例子:

  1. int main()
  2. {
  3. int a[10] = {};
  4. int *p[10] = {};//具有10个指针元素的数组p
  5. int (*ptr)[10] = &a;//指向具有10个元素的数组a的指针
  6. int (&ref)[10] = a;//指向具有10个元素的数组a的引用
  7. for (int i = 0; i < 10; ++i) {
  8. cout << ref[i] << endl;
  9. }
  10. }

关于函数模板的几点说明:

0) 函数模板的参数不是一定要是模板参数,也可以是普通参数。比如下面的例子:

  1. template <typename T>
  2. std::ostream &print(std::ostream &os, const T &obj) {
  3. return os << obj << std::endl;
  4. }
  5. int main()
  6. {
  7. print(std::cout, 100); //打印100
  8. return 0;
  9. }

这里的os就是普通的参数。

1) 函数模板可以声明为inline或者constexpr,关键字inline和constexpr需要放在模板参数列表之后返回类型之前。

2) 函数模板的声明和定义通常都包含在头文件中。

3) 普通函数参数存在隐式地转换,对于模板参数,隐式转换几乎不存在,只有两种情况会发生转换,一是非const对象的引用或者指针转换成const的引用或指针;二是数组或者函数类型的实参会转换成相应的指针。

4) 当使用函数模板去初始化一个函数指针或为函数指针赋值时,编译器使用指针的类型来推断模板实参(有点反推的意味)。下面是一个例子:

  1. template <typename T> int compare(const T &v1, const T &v2) {
  2. return 0;
  3. }
  4. int main()
  5. {
  6. int(*p)(const int &, const int &) = compare;
  7. return 0;
  8. }

main()函数中的compare函数模板中,T的类型就是int,它通过反推函数指针p的类型而得到。

但是下面这种情况,compare就得不到正确的类型,需要特别处理:

  1. template <typename T> int compare(const T &v1, const T &v2) {
  2. return 0;
  3. }
  4. void func(int(*)(const std::string&, const std::string&)) {};
  5. void func(int(*)(const int&, const int&)) {};
  6. int main()
  7. {
  8. //func(compare);//报错:有多个重载函数 "func" 实例与参数列表匹配:
  9. //此时需要指定类型:
  10. func(compare<int>);
  11. func(compare<std::string>);
  12. return 0;
  13. }

5) 函数模板可以被另一个函数模板或者普通的函数重载。

类模板

下面是类模板的一个例子:

  1. #include <iostream>
  2. #include <vector>
  3. using std::vector;
  4. using std::cout;
  5. using std::endl;
  6. template <typename T> class CLS {//定义部分写法
  7. private:
  8. vector<T> *data;
  9. int size;
  10. public:
  11. CLS();
  12. ~CLS();
  13. int getSize() {
  14. return size;
  15. }
  16. void addData(T t);
  17. };
  18. template <typename T> //实现中要带template关键词和参数列表
  19. CLS<T>::CLS() { //注意这里的CLS<T>
  20. size = 0;
  21. data = new vector<T>();
  22. }
  23. template <typename T>
  24. CLS<T>::~CLS() {
  25. delete data;
  26. }
  27. template <typename T>
  28. void CLS<T>::addData(T t) {
  29. data->push_back(t);
  30. ++size;
  31. for (auto i = data->begin(); i != data->end(); ++i) {
  32. cout << *i << endl;
  33. }
  34. }
  35. int main()
  36. {
  37. CLS<int> a;
  38. cout << a.getSize() << endl;
  39. a.addData(2);
  40. a.addData(3);
  41. cout << a.getSize() << endl;
  42. return 0;
  43. }

几个需要注意的点:

1) 本例创建的模板类,它本身也包含了其它的模板类,就是std::vector。

2) 在类外定义的成员函数,也需要用template来声明,并且所属的类要像上例中那样写,即类似CLS::。但是在类模板的作用域内部,就不需要一定写,比如下面的例子:

  1. template <typename T> class CLS {//定义部分写法
  2. private:
  3. vector<T> *data;
  4. int size;
  5. public:
  6. CLS();
  7. ~CLS();
  8. int getSize() {
  9. return size;
  10. }
  11. void addData(T t);
  12. CLS& returnType();
  13. };
  14. template <typename T>
  15. CLS<T>& CLS<T>::returnType() {//这里的<T>一定要写
  16. CLS ret = *this;//但这里已经在类模板的作用域内了,就可以不写<T>,不过写了也不会错
  17. return ret;
  18. }

另外需要注意的是,类模板实例化之后,成员只有在被实际用到的时候才会真正实例化,如果没有使用到,那么即使它是错的 ,编译器也不知道。

还需要注意:上例的CLS中,假设T是string类型,而我们想要使用string::size_type的类型,因此在类模板中就需要使用T::size_type,但是编译器并不知道T的定义,因此它无法识别size_type到底是一个static数据成员还是一个类型成员。默认情况下c++语言假定作用域运算符访问的名字不是类型,那么为了表示size_type是类型,就需要加关键字typename,因为要访问T类型的static数据类型size_type,就需要使用typename T::size_type。

3) 使用不同的类型T,会得到完全没有关系的类,比如说CLS和CLS就是两个完全独立的类。下面有一个例子:

  1. template <typename T>
  2. class Blob {
  3. public:
  4. T _t;
  5. Blob(T t) :_t(t) {}
  6. };
  7. void func1(Blob<std::string>) {
  8. return;
  9. }
  10. int main()
  11. {
  12. Blob<char *> b("hello");
  13. func1(b);
  14. return 0;
  15. }

由于Blob和Blob是两个不同的类,因此就会报错:

  1. 错误 C2664 void func1(Blob<std::string>)”: 无法将参数 1 从“Blob<char *>”转换为“Blob<std::string>”
  2. 错误(活动) 不存在用户定义的从 "Blob<char *>" "Blob<std::string>" 的适当转换

从这里的错误看,实际上如果有转换器也是OK的。比如标准库中的pair:

  1. void f(std::pair<int, const char *>) {}
  2. void g(std::pair<const int, std::string>) {}
  3. int main()
  4. {
  5. std::pair<int, const char*> p(10, "hello");
  6. f(p);//ok,直接有参数匹配
  7. g(p);//ok,可以调用std::pair中的隐式类型转换
  8. return 0;
  9. }

4) 在使用typedef的时候,也要注意,我们可以声明CLS的别名,但是不声明CLS的别名,因为它本身不是类。

默认模板实参

在c++11中,函数模板和类模板都可以有默认实参;而在之前的版本中,只有类模板有默认实参。

下面是函数模板的例子:

  1. template <typename T, typename F = less<T>>//F为默认模板实参,它是一个可调用对象
  2. int compare(const T &v1, const T &v2, F f = F()) {//f是默认函数实参,来的类型是F
  3. if (f(v1, v2))
  4. return -1;
  5. if (f(v2, v1))
  6. return 1;
  7. return 0;
  8. }

下面是类模板实参的例子:

  1. template <typename T = int>
  2. class Number {
  3. public:
  4. Number(T v = 0) : val(v) {}
  5. private:
  6. T val;
  7. };
  8. int main() {
  9. Number<> n;//表示类型是默认的Number<int>
  10. Number<double> nn;//类型是指定的Number<double>
  11. return 0;
  12. }

成员模板

一个类,无论是普通类还是模板类,都可以包含本身是模板的成员函数。这种成员函数称为成员模板。

成员模板不能是虚函数。

下面是普通类中的一个成员模板:

  1. class DebugDelete {
  2. public:
  3. DebugDelete(std::ostream &s = std::cerr) :os(s) {}
  4. template <typename T> void operator()(T *p){
  5. os << "deleting ptr" << std::endl;
  6. delete p;
  7. }
  8. private:
  9. std::ostream &os;
  10. };
  11. int main() {
  12. double *p1 = new double;
  13. DebugDelete dd;
  14. dd(p1);
  15. int *p2 = new int;
  16. dd(p2);//这样用是否OK?
  17. return 0;
  18. }

这里有一个问题,对于一个实例dd中,到底包含了多少的成员函数呢?因为这里对于dd来说,它的operator()参数是int *和double *貌似都是OK的,因为编译和运行都没有报错。

下面是类模板中的成员模板:

  1. template <typename T>
  2. class Blob {
  3. public:
  4. template <typename It> Blob(It a, It b);//类模板中的函数模板
  5. };
  6. template <typename T>//类模板参数列表在前
  7. template <typename It>//成员模板参数列表在后
  8. Blob<T>::Blob(It a, It b) {
  9. ///
  10. }

这里定义的函数模板是一个构造器,可以像下面的代码那样使用:

  1. int main() {
  2. int a[] = { 1, 2, 3, 4 };
  3. Blob<int> Blob(std::begin(a), std::end(a));
  4. return 0;
  5. }

发表评论

表情:
评论列表 (有 0 条评论,72人围观)

还没有评论,来说两句吧...

相关阅读

    相关 16-模板编程

    模板和泛型编程 面向对象的编程和泛型编程都能够处理在编写程序时,不清楚类型的情况;面向对象的编程处理的类型在程序运行之前都是未知的,泛型编程在程序编译时,就可以清楚