侧边栏壁纸
博主头像
Into The Abyss 博主等级

My Life is a Death Race

  • 累计撰写 34 篇文章
  • 累计创建 7 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

c++_study_template

Administrator
2023-10-28 / 0 评论 / 0 点赞 / 194 阅读 / 0 字

模板与泛型

所谓泛型编程,是以独立于任何特定的数据类型的方式编写代码。使用泛型编程时,我们需要提供具体程序实例所操作的类习惯或者值。

模板是泛型编程的基础,模板是创建类或函数的蓝图或者公式,我们给这些蓝图或者公式提供足够的信息,让这些蓝图或者公式真正的转变为具体的类或者函数,这种转变发生在编译时。

模板支持将类型作为参数的程序设计方式,从而实现了对泛型程序设计的直接支持。也就是说,c++模板机制允许再定义类、函数是将类型作为参数。

模板一般分为函数模板和类模板

模板

函数模板

//两数相加
int addfunc(int a, int b){
    int he = a+b;
    return he;
}

double addfunc(double a, double b){
    double he = a+b;
    return he;
}    // 整数和小数相加逻辑相同,代码重复

//函数模板
template<typename T>
T tempfunc(T a, T b){
    T he = a+b;
    return he;
}

模板定义使用template关键字开头,后边跟<>,<>里面叫模板参数列表(模板实参)。如果模板参数列表里有多个参数,则用逗号隔开;<>里面至少得有一个模板参数,模板参数前面有个typename/class关键字,class不是定义类;如果模板参数里边有多个模板参数,那就要用多个typename/class,例如template <typename A,typename B>

模板参数列表里面表示在函数定义中要用到的类型或者,也和函数参数列表类似。那我们用的时候,就得指定模板实参,指定的时候使用<>把模板实参包起来。有的时候有不需要指定模板实参,因为系统能够根据一些信息推断出来

tempfunc这个函数声明了一个名字为T的类型函数,T实际是类型,这个T究竟代表什么类型,编译器在编译的时候会根据针对tempfunc()的调用来确定

函数模板的调用和函数调用区别不大,调用的时候,编译器会根据你调用这个函数模板时的实参区推断模板函数里的参数(形参)的类型,所以,模板函数有时是推断出来的,有时候是需要<>提供的,编译器推断出来这个模板函数的形参类型之后,会为我们实例化一个特定版本的函数

//模板函数调用
int he = tempfunc(1,3);  //1,3为int,编译器可以自己推断模板形参为int
double he2 = tempfunc(1.1f,3.3f);  //1.1,3.3为double,编译器可以自己推断模板形参为double
double he3 = tempfunc(1,3.3f);  //报错,不能推断

在模板参数列表里边,还可以定义非类型参数,非类型参数代表的是一个值。既然非类型参数代表一个值,那么肯定不能使用typename/class来修饰。非类型参数需要用传统的类型名来修饰,例如template <typename A,int B>

当模板实例化时,这种非类型参数的值(或用户提供,或编译器推断),这些值必须是常量表达式,因为实例化这些模板是编译器在编译时实例化的

template<int a,int b>
int addfuncv2(){
    int he = a+b;
    return he;
}

int he = addfunc(1,3); //报错,非类型不不能这样使用
int he2 = addfuncv2<1,3>();  //成功调用,显式指定模板参数
int a = 1;
int he3 = addfuncv2<a,3>();  //不可以,值必须是编译的时候可以确定,a是在运行时给一个初始值1

/***********************************************************/
template<typename T,int a,int b>
int addfuncv3(T c){
    int he = (int)c+a+b;
    return he;
}
int he4 = addfuncv3<int,1,2>(3);   //成功

/***********************************************************/
template<unsigned L1, unsigned L2> //没有参数类型,只有非类型模板参数
int charscomp(const char(&p1)[L1], const char(&p2)[L2]){
        return strcmp(p1,p2);
}
int result = charscomp("test2","test"); //没有提供非类型模板参数,系统会根据test2的长度6,test长度5,取代L1,L2

模板函数可以使内联的,inline位置在模板参数列表之后

template<typename T,int a,int b>
inline
int addfuncv3(T c){
    int he = (int)c+a+b;
    return he;
}

模板定义并不会导致编译器生成代码,只有在我们调用这个函数模板时,才使编译器为我们实例化一个特定版本的函数

函数模板定义可以放在头文件中

类模板

用类模板实例化一个特定的类,实现同一套代码,应付不同的数据类型,精简代码。

编译器不能为类模板推断模板参数类型,所以为了使用类模板,我们必须在模板名后面用<>来提供额外信息,这些信息对应着模板参数列表里面的参数

类模板定义

template <typename A,typename B,...>

class 类名{}

实例化类模板的时候,必须要有类的全部信息,包括类模板中成员函数的函数体

类模板成员函数,可以写在类模板定义中,这种成员函数会被隐式的声明为内联函数

类模板一旦被实例化之后,那么这个类模板的每个实例都会有自己版本的成员函数,所以,类模板的成员函数具有和这个类模板相同的模板参数。一个类模板虽然可能有很多个成员函数,但是当实例化模板之后,如果后续没有使用到某个成员函数,则这个成员函数不会被实例化

如果要把类模板成员函数的定义(函数体)写在类模板定义的外面,那么这个成员函数的模板参数就体现出来了

//myvector.h
#ifndef _MYVECTOR_H_
#define _MYVECTOR_H_
//类模板定义
template<typename T>
class myvector
{
public:
    typedef T* myiterator;  //迭代器
    myvector();  // 构造函数
    myvector& operator=(const myvector&); //赋值运算符重载,在类模板内部使用模板名并不需要提供模板参数
    myiterator mybegin(); //迭代器起始位置
    myiterator myend(); //迭代器最后元素的下个位置
    void myfunc() {};  //成员函数的函数体放在类模板定义中
};

template<typename T>
myvector<T>::myvector()
{
    //构造函数,成员函数的函数体放在类模板定义之外
}

template<typename T>
myvector<T>& myvector<T>::operator=(const myvector&) //<T>返回一个实例化的myvector
{
    return *this;
}
#endif

非类型模板参数

template<typename T, int size =10>
class myarray{
public:
    void myfunc();
private:
    T arr[size];
};

template<typename T, int size>
void myarray<T,size>::myfunc()
{
    cout<<size<<endl;
}
myarray<int ,100> tmparr; //成功
tmparr.myfunc();  //100
myarray<int> tmparr2; //成功
tmparr2.myfunc();  //10

非类型模板参数有一些限制

1)浮点类型不能再非类型模板参数,比如float,double

2)类类型不能做非类型模板参数

typename使用场合

1)typename可用于类模板和函数模板中,表明其后的模板参数是类型参数,可以替换为class

2)使用类的类型成员,用typename来表示这是一个类型,不可以替换为class

::作用域运算符,访问类中的静态成员变量,访问类型成员

template<typename T>
class myvector
{
public:
    typedef T* myiterator;  //迭代器  类型成员
    myvector();  // 构造函数
    myvector& operator=(const myvector&); //赋值运算符重载,在类模板内部使用模板名并不需要提供模板参数
    myiterator mybegin(); //迭代器起始位置
    myiterator myend(); //迭代器最后元素的下个位置
    void myfunc() {};  //成员函数的函数体放在类模板定义中
};

template<typename T>
// myvector<T>::myiterator myvector::mybegin()  //报错,不能分辨类型成员还是静态成员
// ::访问类型成员myiterator(typedef)
typename myvector<T>::myiterator myvector::mybegin()  //不报错,typename显式指定为类型
{

}

函数指针做其他函数的参数

函数指针要想当作函数的参数就需要

typedef int(*functype)(int,int);  //定义一个函数指针类型

int mf(int a, int b)
{
    //实现逻辑
    return 0;
}

void testfunc(int a, int b,functype funcpoint) //funcpoint就是函数指针
{
    int result = funcpoint(a,b);  //相当于调用函数
    cout << result <<endl;
}

int main()
{
    testfunc(3,4,mf);  //0
    return 0;
}

函数模板趣味用法案例

typedef int(*functype)(int,int);  //定义一个函数指针类型

int mf(int a, int b)
{
    //实现逻辑
    return 0;
}

template<typename T, typename F>  //F可推断为函数指针,也可以是可调用对象
void testfunc(const T &i,const T &j,F funcpoint)
{
    cout<<funcpoint(i,j)<<endl;
}

int main()
{
    testfunc(3,4,mf);  //0
    return 0;
}

默认模板参数

1)类模板,类模板名后边必须用<>来提供额外地信息,<>表示这是一个模板。

template<typename T=string, typename F=int>
class myarray{
public:
    void myfunc();
private:
    T arr[10];
};

myarray<> abc; //使用默认模板参数
myarray<> def; //提供一个非缺省值,只提供一个,第二个用的非缺省值

2)函数模板,老标准只允许为类模板提供默认模板参数,c++11新标准允许可以为函数模板提供默认参数

成员函数模板

普通类的成员函数模板

class A   //普通类
{
public:
    template<typename T>
    void myft(T tmp){  //成员函数模板
        cout << tmp << endl;
    }
}

A a;
a.myft(3);   //编译器在遇到这条语句时,编译器才会实例化这个函数模板 

类模板的成员函数模板

类模板的模板参数必须用<>指定,成员函数模板(函数模板)的参数大多数情况下可以推断

template<typename C>  //类的模板参数
class A   //模板类
{
public:
    template<typename T>
    A(T v1, T v2);  //构造函数模板,与整个类的模板C没有关系

    template<typename T2>
    void myft(T2 tmp){  //成员函数模板
        cout << tmp << endl;
    }
    void myfunc(){   //普通函数
        
    }
    C m_c;
}

template<typename C>   //类模板参数列表
template<typename T>   //构造函数自己的参数列表
A<C>::A(T v1, T v2)
{
    cout << v1 <<"   "<<v2<<endl;
}

A<float> a(1,2);

类模板的成员函数(包括普通成员函数/成员函数模板)只有为程序所用(代码中出现该成员函数的调用)才会进行实例化,如果某函数从未使用,则不会实例化

模板显式实例化,模板声明

为了防止在多个.cpp文件中都实例化相同的类模板,c++11提出了一种解决方法,我们称为显式实例化。通过显式实例化来避免这种生成多个相同类模板的开销。使用

template A<float>;

使用显式实例化手段中的实例化定义,这种实例化定义只需要在一个.cpp文件中写即可。

编译器遇到上述代码直接实例化一个A

在其他要使用的.cpp文件中,使用显式实例化手段中的实例化声明,如下

extern的作用:不会再本文件中生成一个extern后边所表示的模板的实例化版本代码,告诉编译器,在其他文件中已经有了一个该模板的实例化版本

extern template A<float>;

模板的实例化定义只能有一个,实例化声明可以有多个,但是不推荐这种特色

using定义模板别名

typedef:一般用来定义类型别名,typedef unsigned int uint_t;

typedef std::map<std::string,std::string> map_s_s;

我们希望定义一个类型,使得typedef std::map<std::string,std::string> map_s_s;前边的std::string固定不变。后面的std::string类型自己指定

template<typename wt> 
struct map_s
{
    typedef std::map<std::string, wt> type;
};

map_s<int>::type map1; //等价于std::map<std::string, int> map1;

这种实现方法有点繁琐,需要创建一个类。在c++11中,可以采用下面的方式创建

template<typename T>
using str_map_t = std::map<std::string,T>;  //str_map_t为类型别名

str_map_t<int> map2;

using用来给一个类型模板起别名,using再用于定义类型(定义类型模板)的时候,是包含了typedef的所有功能,using uint_t = unsigned int;

using map_s_s = std::map<std::string,std::string>;

显式指定模板参数

template<typename T1,typename T2,typename T3>
T1 sum(T2 i, T3 j){
    T1 result = i+j;
    return result;
}

T3 sum2(T1 i, T2 j){
    T3 result = i+j;
    return result;
}

auto result = sum(2000000000,2000000000); //报错
auto result = sum<int>(2000000000,2000000000); //不报错,答案错误,超出int范围
auto result = sum<double>(2000000000,2000000000); //不报错,答案错误,i+j得到整形超出范围,转换为double还是错
auto result = sum<double,double,double>(2000000000,2000000000); //正确,手工指定的优先

sum2<double,double,double>(2000000000,2000000000);//必须写全三个模板<,,double>这种写法

类模板特化

特化:与泛化相反,泛化的一个例子就是模板,可以随便指定类型。    特化指的是对特殊的类型(类型模板参数)进行特殊的对待,给他开小灶,写适合它的专用代码

必须先有泛化版本才能存在特化版本,只要涉及特化,一定先存在泛化

类模板全特化

常规全特化,全特化指的是所有类型模板参数都得有具体的类型代表。特化版本编译器会优先选择,特化版本可以任意多

必须先有泛化版本才能存在特化版本,只要涉及特化,一定先存在泛化

template<typename T,typename U>
struct TC{
    void functest(){
        cout<<"泛化版本"<<endl;
    }
};

//当T与U两个参数都为int类型时,特化版本
template<> //所有类型模板参数都有具体的类型代表,所以<>里面为空
struct TC<int,int>
{
    //在这里可以对该特化版本做单独处理
    void functest(){
        cout<<"int,int特化版本"<<endl;
    }
};

template<> //所有类型模板参数都有具体的类型代表,所以<>里面为空
struct TC<double,int>
{
    //在这里可以对该特化版本做单独处理
    void functest(){
        cout<<"double,int特化版本"<<endl;
    }
};

特化成员函数而不是模板

template<>  // 泛化版本对象,特化成员函数
void TC<double,double>::functest(){
    cout<<"double,int特化版本"<<endl;
}

类模板偏特化(局部特化)

偏特化从两方面说起:

1)模板参数数量

2)模板参数范围

template<typename T,typename U, typename W>
struct TC{
    void functest(){
        cout<<"泛化版本"<<endl;
    }
};

//从参数数量上进行偏特化
template<typename U>  //另外两个参数被绑定到具体的类型
struct TC<int,U,double>
{
    //在这里可以对该特化版本做单独处理
    void functest(){
        cout<<"int,U,double偏特化版本"<<endl;
    }
};

//从参数范围上进行偏特化:int,const int(比int小);T,T*(指针类型T*比任意类型T小)
template<typename T>
struct TC1{
    void functest(){
        cout<<"泛化版本"<<endl;
    }
};

template<typename T>
struct TC1<const T>{
    void functest(){
        cout<<"const T特化版本"<<endl;
    }
};

template<typename T>
struct TC1<T *>{
    void functest(){
        cout<<"T *特化版本"<<endl;
    }
};

template<typename T>
struct TC1<T&>{
    void functest(){
        cout<<"T&特化版本"<<endl;
    }
};

template<typename T>
struct TC1<T&&>{
    void functest(){
        cout<<"T&&特化版本"<<endl;
    }
};

TC1<const double *> tpd;
tpd.functest();  // 调用T*版本

局部特化,特化完成之后它本质上还是一个模板;全特化,特化完成之后就成了一个具体的类

函数模板特化

函数模板全特化

template<typename T,typename U>
void tfunc(T &tmpt,U &tmpu){
    cout<<"函数泛化版本"<<endl;
    cout<<tmpt<<"  "<<tmpu<<endl;
}

//全特化版本
template<>
void tfunc(int &tmpt, double &tmpu){
    cout<<"int,double函数特化版本"<<endl;
    cout<<tmpt<<"  "<<tmpu<<endl;
}

const char *p = "i love china";
int i = 12;
tfunc(p,i)  //函数泛化版本\ni love china  12
double db = 15.8f;
tfunc(i,db); //int,double函数特化版本\n 12  15.8

当既存在重载函数,又存在特化版本时,会优先调用重载函数。编译器的选择:普通函数,特化版本,泛化版本

函数模板不能偏特化

模板特化版本和模板泛化版本都应该防止在同一个.h文件中,前边放泛化版本,后面放特化版本

可变参函数模板

允许模板中含有0个到任意个参数,在语法上也和传统的模板不一样

template<typename... T>
void myfunc(T... args)
{
    cout<<sizeof...(args)<<endl;
    cout<<sizeof...(T)<<endl;
}

myfunc();   // 0    0
myfunc(10,25.8,"abc",68); //4    4

我们一般把args称为一堆参数,而且这些参数的类型各不相同;我们理解T这种类型时,不能把他当做一个类型,而是0个到多个不同的类型,对应的参数args也应该是多个不同类型的参数;参数包的展开一般是用递归函数的方式来展开参数,要求我们在代码编写中,有一个参数包展开函数和一个同名的递归终止函数

//递归终止函数
void myfunc()
{
    cout<<"递归终止"<<endl;
}
template<typename T,typename... U>
void myfunc(const T& firstarg,const U&... otherargs) //一个参数,一包参数最适合参数包展开
{
    cout<<sizeof...(otherargs)<<endl;
    cout<<"收到的参数值为:"<<firstarg<<endl;
    myfunc(otherargs...); //递归调用
}

可变参类模板

允许模板中含有0个到任意个参数

template<typename...args> class myclasst{}; //主模板
template<typename T,typename... U>
class myclasst<T, U...>:private myclasst<U...>
{
public:
    T m_i;
    myclasst():m_i(0)
    {
        cout<<"myclasst::myclasst()"<<endl;
    }
};

myclasst<int,double,float> myc;

模板模板参数

template<typename T,typename U>  //T、U叫模板参数,因为他们前面有typename,所以又叫类型模板参数
class myclass
{
public:
    T m_i;
};

template<
    typename T,
    template<class> class Container
    // template<typename W> typename Container //等价于上式,W可以省略
    >  
class myclass
{
public:
    T m_i;
    Container<T> myc;  //Container作为一个类模板来使用
    myclass()
    {
        for(int i=0;i<10;i++)
            myc.push_back(i);
    }
};

template<typename T>
using myvec = vector<T,allocator<T>>;  //套路写法
template<typename T>
using mylist = list<T,allocator<T>>;  //套路写法

// myclass<int,vector> myvecobj1; //报错
myclass<int, myvec> myvecobj;
myclass<int, mylist> mylistobj;


0
c++

评论区