C++模板

一、泛型编程

举个例子:假如我们有两个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;
}

使用函数重载可以解决这个问题,但存在不好的地方:

  1. 假如我们新增一个需求,有两个float类型数据,要对它们进行相加,这时我们又要重载一份对于float类型数据的add函数重载。使得代码重复性高,复用率低,过于冗余。

  1. 代码可维护性低,一个地方出错,对于重载函数的不同数据类型的版本,都得进行修改。

泛型编程:编写与数据类型无关的通用代码,提高代码复用率,可维护性,是提高代码复用性的一种手段。

模板是泛型编程的基础,C++模板有两种:函数模板和类模板

二、函数模板

函数模板代表了一个函数家族,该函数模板与数据类型无关。程序在编译阶段,编译器会根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数,程序在运行的过程中,使用到函数模板的地方,就会去调用特定版本的模板函数。

注意:函数模板不是一个函数,它不是具体要调用的某一个函数,而是一个模板。程序在编译阶段,编译器会根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数。

  1. 函数模板语法

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;
}
  1. 函数模板原理

C++的程序编译有预编译,编译,汇编,链接四个过程。程序在编译阶段,编译器会根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数,程序在运行的过程中,使用到函数模板的地方,就会去调用特定版本的模板函数。

我们在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;
}
  1. 函数模板实例化

程序在编译阶段,编译器根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数的过程,叫做函数模板实例化。

  • 隐式实例化:让编译器根据实参的数据类型推演模板函数参数的实际类型

#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;
}
  1. 同名模板函数与同名函数

  • 当程序中有同名,同参数类型的函数时,没有显式指定用模板函数的前提下,会调用同名函数。而且编译器也不会生成跟这个同名函数相同参数类型的模板函数。

#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;
}
  1. 多参数函数模板

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

三、类模板

类模板代表了一个类家族,该类模板与数据类型无关。程序在编译阶段,编译器会根据指定的模板数据类型生成特定版本的模板类,程序在运行的过程中,使用到类模板的地方,就会去调用特定版本的模板类中相对应的函数。

注意:类模板不是一个类,它不是具体能够实例化的某一个类,而是一个模板。程序在编译阶段,编译器会根据指定的模板数据类型生成特定版本的模板类,程序在运行的过程中,使用到类模板的地方,就会去调用特定版本的模板类中相对应的函数。

  1. 类模板语法

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();
}
  1. 类模板原理

C++的程序编译有预编译,编译,汇编,链接四个过程。程序在编译阶段,编译器会根据指定的模板数据类型生成特定版本的模板类,程序在运行的过程中,使用到类模板的地方,就会去调用特定版本的模板类中相对应的函数。

我们在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;
}
  1. 类模板实例化

类模板必须在类名后用<>,在<>里面显式的指定类模板的实际参数类型进行实例化,编译器是无法跟部分函数模板那样,能够用实参去推演模板函数的数据类型的。

int main(int argc, char *argv[])
{
    //Point point_i(1, 2); //错误:类模板必须显式指定参数数据类型

    Point<int> point_i(1, 2); //正确
    point_i.print();
}
  1. 同名的类模板与同名的类是不能共存的,编译器会报错。

  1. 类外定义模板类的相关成员函数

#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>

  1. 类模板应用举例:实现一个支持多维的点模板类

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

四、模板形参类型

模板形参类型分为:类型形参和非类型形参

  • 类型形参:出现在模板参数列表中,跟在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;
}

五、模板特化

一般来说使用模板可以实现一些与数据类型无关的代码,但对于某些特殊的数据类型可能会得到错误的结果,这时就要对模板进行特化处理。即:在原有函数模板或类模板的基础上,对特殊的数据类型进行重写,覆盖掉模板的实现方式。

模板特化分为函数模板特化和类模板特化。

  1. 函数模板特化

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
}

同一字符串比较的结果却是不相同的,因为指针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
}
  1. 类模板特化

类模板特化分为全特化和偏特化

全特化:将模板参数列表中所有的参数都确定化

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

偏特化:将模板参数列表中的部分参数确定化

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

六、模板函数,类模板的成员函数声明,定义分离,导致的编译链接问题

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 //链接的过程中出错

解决方法:模板函数,类模板的成员函数声明,定义都放在.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

七、总结

主要内容

C++模板分为函数模板和类模板
  • 函数模板代表了一个函数家族,该函数模板与数据类型无关。程序在编译阶段,编译器会根据显式的模板数据类型(或实参的数据类型来推演)生成特定版本的模板函数,程序在运行的过程中,使用到函数模板的地方,就会去调用特定版本的模板函数。

  • 类模板代表了一个类家族,该类模板与数据类型无关。程序在编译阶段,编译器会根据指定的模板数据类型生成特定版本的模板类,程序在运行的过程中,使用到类模板的地方,就会去调用特定版本的模板类中相对应的函数。

优缺点

优点:使用模板可编写与数据类型无关的通用代码,提高代码复用率,可维护性。C++标准模板库(STL)主要就是运用了C++模板技术来实现。

缺点:

  • 模板会导致代码膨胀问题,导致编译时间变长

  • 当模板编译出错时,错误信息凌乱,不容易定位错误出现的地方

  • 模板不支持分离编译,对想屏蔽不让客户看到的实现代码不友好

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值