C++Primer笔记 十六 模板与泛型编程

模板是泛型编程的基础,同时也是C++与众不同的一个特性。
模板的使用分为两大方向:函数模板和类模板。C++标准库算法是基于函数模板,而标准容器的实现基于类模板。
函数模板(function template):

       包含一个模板参数列表(template parameters list),其中包含一个或多个模板参数(template parameters)。形如:

template <typename T>
int func(const T&){
//
}
模板参数表示我们在定义函数(或者类)时,用到的类型参数。当我们调用一个模板函数的时候,编译器会根据函数实参的类型推断出模板实参,即在彼时,用调用实参的类型来绑定到模板参数T的类型,过程称为推断(deduction)。而完成一个调用实参向模板参数的绑定,完成函数的调用的过程,称为实例化(insantatiate)一个特定的版本。

此外,在模板参数列表中,出了类型模板参数,还可以包含非类型模板参数(non-type parameters),可以通过一个特定的类型名而非关键字typename 和 class来实现。非类型模板参数的模板实参必须是常量表达式。例如可以用来指定数组的大小。形如:

template<unsigned M, unsigned N>
int compare(const char(&p1)[M] , const char(&p2)[N]){   
           return strcmp(p1,p2); 
}
TIPS:

函数模板也可以实现为inline和constexpr的形式,分别放在模板参数列表之后,返回类型之前。

在compare的实现过程中(C++Primer前几章),体现了泛型编程的其中一个原则,模板中的函数形参都是const引用,可以用来绑定不能拷贝的对象。

并且,函数模板和类模板成员函数的实现都应该放在头文件中。


类模板:

首先是类模板与函数模板的一个重要的不同,编译器不负责为类模板推断模板参数类型。为了使用类模板,我们必需在模板名后面的尖括号中提供额外信息,用来代替模板参数的模板实参列表。

当使用一个类模板时,我们提供的额外信息是显式模板实参列表,他们被绑定在模板参数。完成实例化的类。

定义在类内的成员函数隐式地声明为内联函数,而定义在类外的成员函数,必须以关键字加类模板的模板参数列表开头。

有一个关于列表初始化的小细节在p587中,对于列表初始化的容器构造函数,可以理解为构造函数接收一个Initializer_list的参数,并且initializer_list也是一个模板。形如:

template<typename T>
Blob<T>::Blob(std::initializer_list<T> i1)

这里还有一个概念上的区别,就模板名,和类型名。一个类型名总是模板的一个特例化实现,并且总是包含有模板实参的。如Blob<int>,而模板名就是Blob;

通过在类模板的内部定义成员及成员函数,可以避免麻烦,直接使用模板名而不用提供实参。


类模板和友元:

稍微思考一下,就能明白这是一个很讲究的内容。实际上,我们说当一个类包含了一个友元声明的时候,这个类是不是模板,还有这个友元是不是模板都是相互无关的。也就是说最起码,我们可以列出三个不同于平常的形式:非模板类的模板友元(不做本次研究),模板类的模板友元,模板类的非模板友元。

对于模板类的友元可以大致分为以下三种形式。

1.如果友元是非模板的,类是模板的,那么这个友元可以访问该类的所有特例化类型。

2.如果友元是模板的,类可以授权给所有的模板实例,也可以只授权给特定实例。

   (1)对应的一对一关系,即你的int 实例访问我的int 实例,我的string实例只授权给你的string实例。形如:

template<typename t1> class ClassA;

template<typename t1> class ClassB;

template<typename t1> class ClassB{friend class ClassA<t1>;//};

  (2)一对多或者一对全(特例化)的友元关系。

对于一个非模板的类和一个模板类,下面代码分别设置模板类的所有实例作为各自的友元,(及模板类所有实例授权给友元类所有实例的友元关系,俗称“瞎访问”)。

template<typename T>class fnd1;

class C{

//C是一个非模板类,他授权给fnd1所有的特例化类型。

template<typename T> friend class fnd1<T>;

}

template<typename T>

class D{
//D是一个模板类

template<typename X> friend class fnd1<X>;

//fnd1中所有实例是D的所有所有实例的友元。必须使用不同与类模板的模板参数。
}

虽然友元一般是函数或者类的类型,但是在模板中,内置类型的友元关系也可以接受,方便我们在声明类模板参数为该类的友元的时候,也能够用内置类型去实例化这个类。

template<typename T>

class A{ friend T;

}


模板类型别名:可以通过定义模板类型别名实现一族的类型,如template<typename T> using twin = pair<T , T>;使用twin<int >就是pair<int,int>,使用很方便。

类模板的静态成员:类模板的静态成员只在一个实例化的对象之间共享。这个可以从类型名和模板名的概念区别理解。


关于模板参数

作用域问题:模板参数可以隐藏外层作用域(非模板)的相同名字,但在模板中,不能重新用模板参数名。

默认情况下,C++默认认为通过作用域运算符访问的不是类型,因此,为了不混淆我们到底是要访问static成员还是类型成员。我们在访问类型时加上typename的显式声明。

默认参数问题:对默认模板实参的设定也和函数默认参数也是差不多的,如果一个模板为所有参数都设定了默认实参,那么在使用全部默认的情形下,就要在加上参数列表的<>,并且在<>中不放置任何内容。

成员模板:一个类模板(无论模板与否)都可以包含本身是模板的成员函数,称为成员模板(member template),成员模板不能是虚函数。对于非模板的成员模板很好理解也很直观,而对于模板类的成员模板,麻烦的在于模板外的定义。必须同时为类模板和成员模板提供参数,其中类模板参数在前,成员模板参数在后。

template<typename T>
template<typename memT>
Blob<T>::void func(memT a, memTb)//这个东西直观体现了函数模板和类模板的不同,类模板不能隐式推断,函数模板可以。
{}

为了避免在每一个文件中都分别对模板进行特例化的开销,可以显式特例化这些东西(控制实例化)。

形式如同变量的引入等,形同于extern template declaration。template declaration。要注意的是这个与运行时特例化不同在于,必须有且仅有一个地方完成了模板的实例化。并且在特例化时,必须特例化所有成员。与普通的不同!


关于模板实参推断(template argument deduction)

类型转换问题:在使用了模板参数作为形参的函数模板中,能提供的转换只有很少的类型,毕竟对于一个不同的类型,模板只需要选择重新生成一个特例化就好,从int到double的转化是不存在的。即便如此,也有那么两个例外:1、const的转换,顶层const一贯被忽略。可以将一个非const的引用或指针传递给一个const引用或实参。

2、数组或者函数指针的转换,如果一个函数形参不是引用,可以对数组或者函数类型的实参进行正常的指针转换,将数组实参转化为一个指向首元素的指针,将一个函数类型转化为一个该函数类型的指针。

由于模板推断的转换问题,对于同一个模板参数的两个形参,如果提供了非上面两种转换的不同类型,会挂掉。

特别的是,对于一个部分模板参数化的函数,正常的那一部分参数该怎么转换就怎么转化。


函数模板实参推断失效问题:有些时候,比如在返回类型是模板实参的时候,编译器就无法很好的推断出来模板实参的类型,这就告诉我们要有显示指定模板参数的功能。

指定的方法如下:

template<typename T1, typename T2, typename T3>

T1 sum (T2, T3);

//编译器无法推断T1的类型,谓之失效。

//想要显示指定的话,每次调用sum的时候,和类模板一样,后面加上<>的信息指定。

auto val13 = sum<long> (i, long);//T2, T3都可以自动推断,并且显示指定了返回值T1为long。

和默认参数的相互关系有一比的是,最好把显式指定的模板参数放前面,要不然都要显式指定一遍(从T1到T3)。

如果我们不想每次都显式指定返回类型,或者说我们做不到的时候,可以考虑尾置返回类型确定的方法。(运用decltype()

如同

template <typename T>

auto fcn(T beg, T end) ->decltype(*beg)

{//do something

    return *beg;
}//我们要返回的是对一个序列中的元素的引用。

注意,此时在编译器遇到函数列表前,都不指导神马是beg,所以要选用尾置返回的方式。在这里,因为解引用运算符(*)返回一个左值,所以实际返回类型是一个左值引用。

为了能够得到正确的类型信息,而不是一个引用,就要使用C++标准库中的类型转换模板(type transformation template)。定义头文件:type_traits。好用的一个模板remove_reference<int &>将返回int类型(作为其type成员返回)。type本身是一个类型,当然也可以用typename 告诉编译器这个表达式返回一个类型作为尾置指定返回类型。


将函数模板作为函数指针的问题:最主要的问题是推断失效或者二义性的问题,考虑下面一种情形。

template<typename T> int compare(const T&, const T&);

void func (int (*)(const int&a, const int& b));

void func(int (*)(const string&a, const string &b));//两个重载的func类型,都接受一个函数指针参数

func(compare);//好,出错了。。因为出现了ambiguous。不知道应该用哪个func的overload版本。
这时候,就可以对compare进行显式模板实参,即func(compare<int>)。


再说实参推断和引用

书上说了重要的两点,通过例子:

template <typename T>void f(T &p);

1.编译器会应用正常的引用绑定规则

2.const是底层的,不是顶层的。

引用推断类型分为左值推断类型和右值推断类型,两个类型在可以传递接收的参数上有所不同。

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

    1).一个函数参数是模板类型参数的普通引用时,如T&,那么只能传递给他一个左值(变量或返回左值的表达式)

此时,实参可以是const或者非const的,const实参推导出 T是const的。

    2).一个函数参数是模板类型参数的const引用时,如const T&,那么从绑定规则可以知道,他可以接收传递的任何参数--(const或者非const对象、一个临时对象、或者一个字面常量值。)

2.从右值引用参数推断

当一个函数参数是一个右值引用时,如T&&,正常绑定规则告诉我们可以传递给他一个右值。当传递给他一个右值时,可以推断T为右值参数的类型。数字常量5推断出 T->>int;

本来问题到这里结束是很容易拎得清的,然而,凡事有例外。关于函数参数的右值引用就有那么几个例外:

分别被称为:一,模板与类型别名的引用之引用推断 二,引用(之引用)的折叠

一、(仅限于模板) 当我们把一个右值引用T&&绑定到一个左值(如 int i)上时,编译器能够正常推断,并且最终推断出T->>int&,即推断出模板类型参数为实参的左值引用。那么,T&&就是对一个左值引用(int &)的右值引用。即引用之引用,并且这种情形只能够通过模板类型参数类型别名来做到(其他人做得到么!)

二、X& && ->>X&  X&& &->>X&  X& & -> X&

X&& &&->>X&&

当我们应用一个模板参数的右值引用作为函数模板的参数时,出现一个神奇的现象。

template<typename T> bool fnc(T&& t1){
T t2 =  t1;
t2++;
return t1 == t2;
}

此时,由于右值的引用,模板参数T可能推断出实参的类型,或者实参的左值引用类型。那么就出现一个问题,t1绑定到的参数是否会改变。当t1绑定到int &时,明显会改变实参。当t1绑定到int时,t2只是一个拷贝,不该表实参。

这个现象可能是有用的,但大部分情况下,我们应该禁止这一个行为不一致的现象,最常见的方法就是通过函数模板的重载。如同非模板版本一样。

template<typename T>void f(T&&);
template<typename T>void f(const T&);
当要绑定到一个可变的右值时,将调用右值引用版本;当要绑定到一个左值或者const右值时,将调用const T&版本。


关于右值引用,最典型的是标准库中的move函数,因为move本质上可以接受任何类型的实参,明显是函数模板

move的作用是对任何类型的实参返回其右值引用的形式,其实现过程如下:

template<typename T>
typename remove_reference<T>::type&& move (T&& t){
return static_cast<typename remove_reference<T>::type&&>(t);
}

move要完成自己的目标,需要保证两个条件:

1.正确恰当推断T,

             a.绑定到右值时,T推断为实参类型(e.g int)。t 就是 int&&

             b.绑定到左值时,T推断为实参类型的左值引用。t 折叠为int &

2.返回右值。 remove_reference后,a和b的情形,type都被推断为原本实参类型(e.g int),加上右值引用就是int &&

返回值是 static_cast<int &&> (int&&  /* or  int& */);

上面也看得出来,从一个左值到右值的 static_cast是被允许的。


完美转发

这也是一个特色内容,在实际应用中,我们有时候会需要将一个或者多个参数连同类型不变地转发给其他函数,因此需要保持被转发参数的所有性质,包括实参类型是否是const的,以及实参是左值还是右值。

版本一

template<typename T>
void func(T t){
     f(t);//调用函数f
}
将t绑定到一个左值时,如 func( i );此时,t 是 i 的一个副本, 如果函数 f 的版本是  f = void ()(int &c)的话,c被绑定到了t绑定实参生成的副本上,而不是t绑定的实参本身。这是第一个问题, 按值传参转发到左值引用时的隔断性。

为了改变这个情形,我们需要另谋出路,不直接使用没有引用T类型。改用T&&版本。

版本二

template<typename T>
void func(T&& t){
     f(t);//调用函数f
}
首先右值引用绑定时,可能推断出的T的结果分别是 int &或者int 类型。因此,t折叠为int &或者int &&。在

f = void ()(int & c)的时候,转发分别保证了左右值的特性,关于const的属性,由于是右值引用的函数参数,则对于不论左值右值的const的属性都被保留。理由是引用中const的底层属性(引用只有底层const)。

但是,当f = void()(int && c)的时候,当我们传递给 c一个右值int &&时,参数c绑定的是函数模板的参数 (形参) ,虽然 参数实参 t 本身是一个右值引用,但函数模板形参的形式是一个左值表达式,等同于普通的变量(理解起来并不直观)。因此,我们传递给 f 函数一个左值。造成不一致。

最终版本:

通过utility头文件中的std::forward函数保持类型信息。通常情况下,传递那些定义为模板参数类型的右值引用的函数参数都会出现不一致的问题,通常用std::forward保持给定实参的左值、右值属性。

template <typename T>
send (T &&arg){
        callF(std::forward<T>(arg));
}
forward应用与一个函数模板参数的右值引用函数参数时(T&& t),std::forward<T>t 能保持t的所有细节信息!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值