C++的模板

模板是C++泛型编程的基础,是编译器生成函数或者类的蓝图或者说公式


定义函数模板:

template <typename T>//T是模板参数,可以有一个或者多个
int compare(const T& ctv1,const T &ctv2)
{
    if(ctv1<ctv2) return -1;
    if(ctv2<ctv1) return 1;
    return 0;
}

实例化函数模板:

//当我们使用模板时,显式或者隐式给出模板实参
cout<<compare(1,0)<<endl;    //T为int

模板参数:

在上面模板中模板参数是T,模板参数可以用来声明函数参数类型、函数返回类型,以及在函数体内变量声明或者类型转换

//返回类型与参数类型相同
template <typename T>
T fun(T* P)     
{
    T tmp=*p;
    return tmp;
}

非类型模板参数:

除了定义类型参数,还可以定义非类型参数,一个非类型模板参数是一个值而非一个类型,非类型参数用特定类型名而非关键字typename或者class指定,在模板实例化时,非类型模板参数被用户提供或者编译器推断出的值来取代(当然,这些值必须是常量表达式才能编译时使用)

例子:

//模板非类型参数可以是整型,或是指向对象或者函数的指针或者(左值)引用
//绑定到整型的实参必须是常量表达式,绑定到指针或者引用非类型参数的实参必须具有静态生存周期,不能用局部(非static)变量或者动态对象来初始化非类型参数
template <unsigned M, unsigned N>
int compare(const char (&p1) [M],const char (&p2)[N])
{
    return strcmp(p1,p2);
}

inline 和constexpr函数关键字在模板参数列表之后,函数返回类型之前


模板编译:编译器遇到模板定义并不生成代码,当我们实例化出模板的一个特定版本时,才会生成代码,与函数和类不同,模板的头文件既包括声明也包括实现。

函数模板和类模板的成员函数定义通常放在头文件中


类模板:

用来生成类,与函数模板不同,编译器不能为类模板提供模板参数类型推断,类模板使用必须在模板名后面的尖括号提供额外信息


定义类模板

template <typename T> class Blob {  
public:  
    typedef T value_type;  
    typedef typename std::vector<T>::size_type size_type;  
    Blob ();  //构造函数
    Blob (std::initializer_list<T> il); //可以使用初始化列表, {}  
    size_type size() const { return data->size(); }  
    bool empty() const { return data->empty(); }  
    void push_back (const T &t) { data->push_back(t); }  
    void push_back (T &&t) { data->push_back(std::move(t)); } //右值操作,移动版本
    void pop_back ();  
    T& back ();  
    T& operator[] (size_type i) ;  
private:  
    std::shared_ptr<std::vector<T> > data;  
    void check (size_type i, const std::string &msg) const; //验证给定的索引  
};  

实例化类模板:

实例化类模板需要提供显式实参列表,编译器使用这些模板实参实例化出特定的类

例如:

Blob<int> ia;  //空Blob<int>
Blob<int> ia2={1,2,3,4,5};// 由初始化列表提供5个元素的Blob<int>

在类模板内部定义的成员函数将是隐式内联的,定义在外部时,应该加上模板参数列表,及所属类。若是在类代码内部可以简化模板名的使用,在自己作用域中,可以直接使用模板名而不提供参数

例如:

template<typename T>
T& Blob<T>::back()
{
....
}


类模板和友元:

当一个类包含一个友元时,类和友元各自是否是模板是无关的。如果一个类模版包含一个非模板友元,则该友元可以访问所有模板实例。如果友元自身是模板,则类可以授权给所有友元实例可以授权给特定实例。

一对一友元:用相同参数实例化,例子如下:

//前置声明,Blob中声明友元需要
template <typename> class BlobPtr;
//预算符==中参数需要
template <typename> class Blob;
template <typename T>
    bool operator==(const Blob<T>&,const Blob<T>&);

template <typename T> class Blob{
    friend class BlobPtr<T>;
    friend bool operator==<T>
        (const Blob<T>&,const Blob<T>&);
    ...
}//前置声明是友元及函数参数所需要的
//友元的声明用Blob的模版形参作为自己的实参,因此友好关系限定在相
//同类型实例化的Blob及BlobPtr相等运算符之间

通用和特定的模版友好关系:一个类也可以将另一个模版的每个实例声明为友元,或者限定特定实例为友元

//前置声明,在将模版的一个特定实例声明为友元时将会用到
template <typename T> class Pal;
class C{//一个普通类
    friend class Pal<C>;//用类C实例化的Pal是C的一个友元
    //Zoo的所有实例都是C的友元(这种情况无需前置声明)
    template <typename T> friend class Zoo;
};



template <typename T> class C2{//C2本身是一个类模版
    //C2将每一个相同实例化的Pal声明为友元
    friend class Pal<T>;
    //Pal2的所有实例都是C2的每个实例的友元,不需要前置声明,模版参数不同
    template <typename X> friend class Pal2;
    //Pal3是C2所有实例的友元,不用Pal3的前置声明
    friend class Pal3;
};

成员模版:一个类(无论普通类还是类模版)可以包含本身是模版的成员函数,这种成员成为成员模版,成员模版不能是虚函数

普通类的成员模版

    class DebugDelete{
    public:
        DebugDelete(std::ostream &s=std::cerr):os(s){}
        //与任何函数模版相同,T的类型由编译器推断
        template <typename T> void operator()(T *p)
        {
            os<<"delete p""<<std::endl;
            delete p;
        }
    private:
        std::ostream & os;
    };
    //使用
    double *p=new double ;
    DebugDelete d;//可以像delete表达式一样使用的对象
    d(p);//调用DebugDelete::operator()(double *) 释放p
    int *ip= new int;
    DebugDelete()(ip);//在一个临时对象上调用operato()(int*)

类模版的成员模版:类模版和成员模版各自有自己的、独立的模版参数

template<typename T> clas Blob{//类模版类型参数
    template<typename It> Blob(It b,It e);//构造函数定义为模版
    //..
};

//当我们在类模版外定义一个成员模版时,必须同时为类模版和成员模版提供模版参数列表,类模版参数列表在前,后跟成员自己的模版参数列表

template<typename T>  //类模版参数列表
template<typename It> //成员模版构造函数的参数列表
    Blob<T>::Blob(It b,It e):data(std::make_shared<std::vector<T>>(b,e)){}
//make_shared<T>(args),返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象

实例化成员模版:必须同时提供类模版和函数模版的实参来实例化一个类模版的成员模版;我们在那个对象上调用成员模版,编译器就根据改对象类型推断类模版参数的实参;与普通函数模版相同,编译器根据传递给成员模版的函数实参来推断他的模版实参;

int ia[]={0,1,2,3,4,5,6,7,8,9};
vector<long> vi={0,1,2,3,4,5,6,7,8,9};
list<const char*> w={"now","is","the","time"};
//实例化Blob<int>类及其接受两个int*参数的构造函数
Blob<int> a1(begin(ia),end(ia));
//
Blob<int> a2(vi.begin(),vi.end());
//
Blob<int> a3(w.begin(),w.end());

控制实例化:

extern template declaration;//实例化声明
template declaration;//实例化定义
//declaration是一个类或者函数声明,其中所有模版参数已被替换为模版实参,例如:
extern template class Blob<string>;     //声明(其他位置定义)
template int compare(const int &,const int &);//定义

//实例化定义会实例化所有成员

模版实参推断:

对于函数模版,编译器利用调用中函数的参数来确定模版参数。从函数实参来确定模版实参的过程称为模版实参推断。

类型转换与模版类型参数

与往常一样,顶层const会被忽略(无论在形参中还是在实参中)其他类型转换能在调用中用于函数模版的包括如下两项

  • const转换:可以将一个非const对象的引用(或者指针)传递给一个const的引用(或指针)形参

  • 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或者函数类型的实参应用正常的指针转换。数组实参可以转换为一个指向其首元素的指针,类似的,一个函数实参可以转换为一个该函数类型的指针>

其他转换(如算术转换、派生类向基类的转换、自定义转换等)都不能和应用于函数模版

template<typename T> T fobj(T,T);//传值,实参被拷贝
template<typename T> T fref(const T&,const T&);//引用
string s1("a value");
const string s2("another value");
fobj(s1,s2);//调用fobj(string,string),实参被拷贝,const被忽略
fref(s1,s2);//调用fref(const string &,const string &),将s1转换为const是允许的

int a[10],b[24];
fobj(a,s2b);//调用fobj(int*,int*)
fref(a,b);  //错误,数组类型不匹配

一个模版形参可以用作多个函数形参的类型,此时传递给函数的实参必须有相同的参数类型,不然参数推导出不同类型,推断失败


函数模版显式实参

有时编译器不能推断出模版实参的类型;有时希望用户控制模版实例化。(函数返回类型与参数列表中类型都不相同时这两个情况最常见)

指定显式模版实参:

//编译器无法推断T1的类型
template<T1,T2,T3>  //没有任何实参类型可以用来推断、T1的类型
T1 sum(T2,T3);
//每次调用sum时,调用者都必须为T1提供一个显式模版实参,
//提供显式模版实参的方式与定义类模版实例的方式相同,显示模版实参在函数名之后,函数实参列表之前
//T1是显式指定的,T2、T3是推断出的
auto val3=sum<long long>(i,long);//long long sum(int,long)

位置返回类型与类型转换:

位置返回类型允许我们在参数列表之后声明函数返回类型(因此可以使用参数列表中的参数);

template<typename It>
auto fcn(It beg,It end)->decltype(*beg)
{
    //处理序列
    return *beg;//返回序列中一个元素的引用
}

进行类型转换的标准库模版类

//remove_reference获取元素类型
remove_reference<decltype(*beg)>::type ;
//去掉了引用,只剩元素类型本身
//为了使用模版参数的成员,必须使用typename
template<typename It>
auto fcn2(It beg,It end)->typename remove_reference<decletype(*beg)>::type
{
...
return *beg;
}

函数指针与实参推断:

当我们用一个函数模版初始化一个函数指针或者为一个函数指针赋值时,编译器根据函数指针类型来推断模版实参

template<typename T> int compare(const T&,const T&);
int (*pf)(const int&,const int&)=compare;
//pf中的参数类型决定了模版实参类型,本例中模版实参类型T是int

当参数是一个函数模版实例的地址时,程序上下文必须满足:对每个模版参数,能唯一确定其类型或者值


模版实参推断和引用:

  • 编译器会应用正常的引用绑定规则;
  • const是底层的,不是顶层的;

从左值引用函数参数推断类型:

当函数参数是模版类型参数的一个普通(左值)引用时,正常的绑定规则告诉我们,只能传递给他一个左值(如一个变量或者一个返回引用类型的表达式),实参可以是const类型也可以不是,如果实参是const,则T被推断为const类型:

template<typename T> void f1(T&);//实参必须是一个左值
//对f1的调用使用实参所引用的类型作为模版参数类型
f1(i);//i是int,模版参数类型T是int
f1(ci);//ci是const int,模版参数T是const int;
f1(5);//错误,传递给一个&参数的实参必须是一个左值

如果一个函数参数的类型是const T&,正常的绑定规则告诉我们可以传递给他任何类型的实参–一个对象(const或者非const)、一个临时对象或是字面常量值。当函数参数本身是const时,推断模版参数结果不会是一个const类型;

template<typename T> void f2(const T&);//可以接受一个右值
f2(i);//i是int,模版参数T是int
f2(ci);//ci是const int,但模版参数T是int;
f2(5);//一个const&参数实参可以绑定到一个右值,T是int

从右值引用函数参数推断类型:

当一个函数参数是一个右值引用,正常绑定规则告诉我们可以传递给他一个右值,类型推断过程类型普通左值引用函数参数推断过程。推断出的模版参数T类型是该右值实参的类型:

template<typename T> void f3(T&&);
f3(42);//实参是一个int类型的右值,T是int

引用折叠和右值引用参数:

假定i是一个int对象,通常情况可能认为f(i)调用不合法,毕竟i是一个左值,而通常我们不能把一个右值绑定到左值上。但C++在正常规则之外定义了两个例外规则,允许这种绑定(这两个例外规则是move这种标准库设施正常工作的基础)

第一个例外规则(影响右值引用参数的推断如何进行):当我们将一个左值(如i)传递给函数的右值引用参数(如T&&)时,编译器推断模版类型参数为实参的左值引用类型。因此,当我们调用f(i)时,编译器推断T的类型为int&,而非int;

> T被推导为int&看起来好像以为着,f的函数参数应该是一个左值int&的右值引用。通常不能直接定义一个引用的引用,但是通过类型别名或通过模版类型参数间接定义是可以的

第二个例外绑定规则:(引用折叠,它只能应用于间接创建的引用的引用,如类型别名或者模版参数)

两个结论:

  • 如果一个函数参数是一个指向模版参数的右值引用(如T&&),则它可以被绑定到一个左值,且:
  • 如果实参是一个左值,则推断出的模版实参类型将是一个左值引用,且函数参数将实例化为左值引用参数

右值引用通常用于两种情况:模版转发其实参或者模版被重载


理解std::move:

//std::move的定义
//返回类型和类型转换中也要用到typename
template<typename T> 
typename remove_reference<T>::type&& move(T&& t)
{
    //static_cast
    return static_cast<typename remove_reference<T>::type&&>(t);

}

重载与模版:

函数模版可以被另一个模版或者非模版函数重载。与往常一样,名字相同的函数必须具有不同数量或者类型的参数

//返回一个给定对象的string表示,**通用版本**
template <typename T> string debug_rep(const T &t)
{
    ostringstream ret;
    ret<<t;
    return ret.str();
}

//**打印指针的版本**(注意:不能用于char*,IO库为char*定义了一个<<版本,此版本假定指针表示一个空字符结尾的字符数组,并打印数组内容,而非指针本身地址值)
template <typename T> string debug_rep(T *p)
{
    ostringstream ret;
    ret<<"pointer:"<<p;//打印指针值
    if(p)
        ret<<""<<debug_rep(*p);//打印指向的内容
    else
        ret<<"null pointer"<<;
    return ret.str();
}


//使用的例子
string s("hi");
cout<<debug_rep(s)<<endl;//只有第一个版本可用,第二个要求指针参数


//使用指针调用debug_rep
cout<<debug_rep(&s)<<endl;

//这种情况两个函数都生成可执行实例:
//debug_rep(const string* &),第一个版本,T被绑定到string*
//debug_rep(string*),第二个版本,T被绑定到string
//第二个精确匹配,第一个需要普通指针到const转换





//另外的例子
const string *sp=&s;
cout<<debug_rep(sp)<<endl

//此例中两个都是精确匹配
//debug_rep(const string *&),第一版,T绑定到string*
//debug_rep(const string *),第二版,T绑定到const string
//虽然都是精确匹配,**但是第二个更特例化**,所以调用第二个
//对于一个调用如果一个非函数模版和一个函数模版提供同样好的匹配,则选择非模版版本(更特例化)

可变参数模版:(需要后续补充)

就是一个接受可变数目参数的模版类或者模版函数。可变数目的参数称为参数包。有两种参数包:模版参数包;函数参数包


模版特例化:

一个特例化模版就是模版的一个独立的定义,在其中一个或者多个模版参数被指定为特定的类型;

//通用版本。可以指定任意类型
template<typename T> int compare(const T&,const T&);
//第二个版本处理字符串字面常量
template<size_t N,size_t M> 
int compare(const char (&)[N],const char (&)[M]);

const char *p1="hi",*p2="hello";
compare(p1,p2);//调用第一个模版
compare("hi","hello");//调用非类型参数的(第二个)

//为第一个提供一个特例化的处理字符指针;
template<>
int compare(const char* const &p1,const char* const &p2)
{
    return strcmp(p1,p2);
}//特例化第一个版本;

当定义函数模版的特例化版本时,我们本质上接管了编译器的工作,即我们为原模版的一个特殊实例提供了定义。一个特例化版本本质是一个实例,而非函数名的一个重载版本。对函数匹配没有影响;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值