Loading

欢乐C++ —— 16. 模板与泛型

参考:

https://blog.csdn.net/k346k346/article/details/49500635 分离式编译与函数模板

https://www.cnblogs.com/QG-whz/p/5132745.html 编译期多态与运行期多态

简述

面向对象 和 泛型编程 都能处理在编写程序不知道类型的情况,不同之处在于,OOP能处理类型在运行时已知的情况,而在泛型编程中,处理的是在编译时已知。

基础概念

函数模板

从简单的比较函数说起,当没有函数模板时,你利用函数重载可以定义以下函数。但这样看起来很繁琐,并且也不可能穷举完所有版本,例如用户自定义类型

bool compare(int a, int b) 
{
	return a > b;
}
bool compare(double a, double b)
{
	return a > b;
}
int compare(const char* a, const char* b)
{
	return strcmp(a,b);
}

但当有了函数模板后,意味着你可以这样做

template<typename T> 
bool compare(T a, T b)
{
	return a>b;
}
int main(){
    int a=1,b=4;
    compare(a,b);		//编译器会实参推断,有实参推演出来类型参数为 int
    compare<int>(a,b);	//直接指定类型参数。
    
    char c1='a',c2='d';
    compare(c1,c2);	
    compare<char>(c1,c2);
    
    vector<int> v1,v2;
    compare(v1,v2);
    compare<vector<int>>(v1,v2);
}
类模板

类是生成对象的蓝图,类模板是生成类的蓝图。与函数模板不同的是编译器不能为类模板推断参数类型。就像我们经常使用的vector ,每次使用必须要加上类型 例如 vector

template<typename T>
class A {
    A(){ }
};
A<int> a;

类模板的作用域内中使用类类型可以不加< >

类型参数与非类型参数

在上面的示例中:T 称为类型参数 ,其前面用 typenameclass 没有什么区别,但为了见名知意,还是使用 typename 更好。

非类型参数,故名思意,其不是类型参数,实参必须为constexpr 常量表达式,静态类型,需要在编译期已知。非类型参数有时也很有用,见下面示例。

模板参数形式类似于函数参数,也可有默认值。

没出现在函数模板的形参列表中的类型参数都是显式模板实参,我们使用时必须通过<>明确指出模板实参,类模板的类型参数就是显式模板实参、而函数模板没出现在函数参数列表中的也是显式模板实参,都需要用户指出。

template <typename T,int N>	//T 为类型参数 N为非类型参数
void print( T (&arr)[N] ) {
	for (auto i : arr ) {
		cout << i << ' ';
	}
	cout << endl;
}
int main( ) {
	int arr[4] = { 1,2,3,4 };
	char brr[2] = { 'a','c' };
	
	print(arr);	//由编译器推断出 print<int,4>(arr);
	print(brr);	//			   print<char,2>(brr);
    print(p); 	//error
}

后面模板元编程中我们会再次见到这个print 函数,在那里,print 函数可以接受任意类型,任意个数的实参。

类模板静态成员

同一类型模板类会公用同一个静态对象,不同类类型静态成员互相无关。

模板类与友元

分几种情况,若该友元也是一个模板类或函数,则定义了一对一友元的关系。

模板作者与使用者

作者要保证模板内不依赖模板参数的所有符号都是可见的。而使用者要保证依赖于模板参数的所有符号都是可见的。

高级概念

声明 实例化 特例化 偏特化
模板实例化

我们无法直接使用模板,在上面的实例中 print 中,看上去我们好像直接使用了函数模板,但实际上,我们使用的是由 print 这个函数模板生成的模板函数,根据模板生成相应 模板函数或模板类 的过程称为 模板实例化

函数模板 / 类模板 本质上是一个模板,不能直接使用,而 模板函数 / 模板类 是通过前者实例化生成的,可以像正常的函数或类来使用。

两类实例化:

  1. 显式实例化:直接声明相应类型的模板函数。

    不管是否发生函数调用,都可以通过显示实例化声明来实例化模板函数。只有在第一次实例化才会有效。

  2. 隐式实例化:在没有相应类型模板函数时,直接调用函数模板。

    当我们调用模板函数时,若没有相应类型模板函数,则编译器会先进行实参推演,再根据模板参数类型实例化模板函数。

//这种直接指定编译器生成相应类型模板函数的方式叫做模板的显式实例化。
template bool compare<int>(int, int);
int main()
{
    compare<int>(10,20);
    compare<double>(10,20);
    /*
    如果此前未实例化相应类型的模板函数,则编译器将会根据指定类型自动生成下面两种模板函数。
    这种又叫做模板的隐式实例化。
    template bool compare<int>(int a, int b){
    	return a > b;
    }
    template bool compare<double>(double, double){
    	return a > b;
    }
    */
}

对类模板来说,其显式实例化,会实例化所有的成员。

而隐式实例化,只有当某个成员函数使用到了才会实例化。这个特性使得某一类型不支持类模板的所有操作,我们依然可以使用。

模板的声明

一般来说,模板在头文件中,对源代码来说本身可见,所以一般很少直接声明模板。而为了减少代码冗余,经常外部声明实例化的模板函数 或 模板类。

若没有显示实例化,则模板使用时才会实例化,这个特点意味着可能有相同的实例化代码出现在不同的源文件中,这是不必要的代码重复。

所以,推荐显式实例化模板,特别是当一个模板要用在多个源文件中。此时应该在一个源文件中显式实例化,而在其它的源文件中外部声明这个实例。

当编译器遇到模板的 extern 外部声明时,它不会在本文件中实例化,而是会去其它文件查找。所以声明有多个,而实例化只可以有一个(多于一个会造成冗余)

template <typename T>
bool compare(T a, T b);

template <typename T>
class A<T>;
//模板的声明

extern template bool ccmp<float>(double, double); //外部声明
extern template class A<float>;//外部声明
//实例化版本的外部声明

typedef A<int> A_Int;	//A<int> 为类型名
template<typename T> using  AA = A<T>; //tmplate<typename T> A<T> 为模板名  
//AA<int> 和 A<int> 等价

此外:模板的名字并不是类型名,只有指定类型参数后,其才是类型名。类型重命名可以用 typedef 而模板重命名要用using

模板特例化

特例化的本质是函数模板的实例化。

我们的compare函数模板,对int,double,char 等类型都能正常比较,但无法比较两个char str[] 字符串的大小。为了使它支持字符串比较,我们需要对它进行特例化。

template<>
bool compare(const char *a, const char *b) {
	return strcmp(a, b) > 0;
}
template<>
bool compare<const char *>(const char *a, const char *b) {
	return strcmp(a, b) > 0;
}

template<>
class A<int>{};

特例化template< >中为空,因为它实际代表的是特例化保留的类型参数,在后文的偏特化你会有更多的认识。

注意,以上两个版本虽然目前都可以正常工作,但最好还是在函数名后加上类型参数,即选取第二种;因为当前的函数可以根据函数参数列表推出类型参数;但如果是显式类型参数,则第一种写法会报错因为无法推断类型参数。

函数模板一般类型参数在形参上,可以根据形参自动推断,所以第一种写法较多。类模板一般类型参数在类内,所以需要显式指出类型参数。

区分函数模板的定义,声明,函数模板的特例化,模板函数的声明

template<typename T> 
bool compare(T a, T b){
	return a>b;
}

template<typename T>
class A { };
//模板的定义

template <typename T>
bool compare(T a, T b);

template <typename T>
class A<T>;
//声明


template bool compare<int>(int  a, int b);
template class A<int>;
//显式实例化

extern template bool ccmp<float>(float, float); 
extern template class A<float>;
//实例化的外部声明


template<>
bool compare<int>(int a, int b) { } 

template<>
class A<int>{ };
//特例化


typedef A<int> A_Int;	//A<int> 为类型名
template<typename T> using  AA = A<T>; //tmplate<typename T> A<T> 为模板名  
//模板名,AA<int> 和 A<int> 等价

总结:定义与声明是相似的;而显式实例化只需要指出template 和 类型参数;特例化则需要 template<> 和类型参数。

对类来说,既可以特例化整个类,也可以只特例化某个成员函数。

偏特化

偏特化也称部分特例化,就是同样在特例化时只指出部分类型参数,只有类模板才有偏特化。

template<typename T1,typename T2>
class Test{

};

template<typename T1>	//保留一个类型参数
class Test<T1,int>{

};
效率与灵活性

在前面动态内存中,我们简单的说了说shared_ptr 和 unique_ptr 。它们都是类模板,都给允许我们自定义资源释放函数,而两者对自定义函数的接口又有所不同。

auto del = [ ] (auto *p) {delete p; };
auto del2 = [ ] (auto*p) {cout << p; delete p; };

shared_ptr<int> p1(new int(3), del);
p1.reset(p1.get( ), del2);

unique_ptr<int, decltype( del )> p2(new int(3),del);

我们可以推断出:

  1. 运行时绑定删除器

    shared_ptr 不是将删除器直接保存为成员,因为直到运行时才能得知删除器类型,并且,我们可以随时改变删除器的类型。而类成员类型通常是不可变的。

    实际上我们可以推断出它必须将删除器间接保存为一个指针,或者一个封装了指针的类,

    shared_ptr 的析构函数有类似于如下的语句:

    del ? del(p):delete p;
    
  2. 编译时绑定删除器

    由于unique 有两个模板参数,一个表示所管理的内存,一个表示删除器的类型。由于删除器类型在编译时就可知,从而可以直接将删除器直接保存为成员。

总的来说,由于指定为模板参数,在编译时就已知类型,保存为成员,从而在运行直接调用,免去开销。而通过运行时绑定,可以使重载删除器更方便,提高适用性。

成员模板

无论是模板类还是普通类,其都有可能包含成员函数模板,也称为成员模板。

下面就是模板类的成员函数模板。

template<typename T>
class A {
public:
	template<typename XT> A(XT x,XT b);
};

template<typename T>
template<typename XT>
A<T>::A(XT x,XT b) {

}
虚函数与函数模板

为什么虚函数不能为函数模板?

简单来说,这是标准主动施加的限制。实现虚函数功能通常是一个固定大小的表,而函数模板决定了不确定的模板函数个数。

类型转换与模板类型参数

模板实参匹配比函数实参匹配规则更为严格。

举个例子,你的函数模板处理两个具有继承关系的两个对象。你可以这样做

class Base{
public:
    int b;
};
class Derived:public Base{
public:
    int d;
};
template<typename T>
void add(const T& a, const T& b){
    std::cout<<"add";
}

int main(){
    Base b;
    Derived d;
    Base *bp = &b;
    Base *dp = &d;

    add(b,d);	//error
    add(*bp,*dp);
    add<Base>(b,d);

    return 0;
}

但这样做的问题是,模板的实参推断可自动执行的转换很有限。

这意味着上面20 行会出错误,原因是T 推断为Base 类型,而d 并不是Base 类型。注意此处比正常的函数实参形参类型转化更为严格。

那为什么第21 行就正确呢?因为dp 和 bp 的静态类型都是Base *类型,当解引用后自然得到Base 类型。

实际上第22行的写法所表示的含义是最明确的,我明确的指出T 为Base 类型,也就是说我是指定了Base 类型的模板函数。此时形参实参匹配就和正常的函数匹配规则一样。

尾置返回类型与模板

有的时候我们想使用模板函数来获得其容器中的元素,考虑下面的示例

如果我们想使用模板函数来查找一个范围内的值,但是接受的是迭代器类型,我们想返回它所指向的元素类型。这块又该如何处理呢。在前面的学习,你可能想到了 decltype(*begin) 但遗憾的是,在编译器遇到begin 参数之前,它是不存在的。

此处就该使用尾置返回类型。

template<typename T>
int find(T begin, T end) {
	auto it = begin;
	//一些查找操作
	return *it;
}
/*错误示例
template<typename T>
decltype(*begin) find(T begin, T end) {
	auto it = begin;
	//一些查找操作
	return *it;
}
*/

template<typename T>
auto find(T begin, T end)->decltype( *begin ) {
	auto it = begin;
	//一些查找操作
	return *it;
}

//但从c++14 开始,你直接可以这样写
template<typename T>
auto find(T begin, T end) {
	auto it = begin;
	//一些查找操作
	return *it;
}
模板的编译

使用函数模板,对编译器来说,并没有节省多少代码,因为对每种使用到的类型都会实例化一个模板函数。但是对程序员来说,可以少写一些代码,并且修改的话更方便。

模板直至实例化才会编译,这个特性影响了何时才能知道模板的错误。总共有三阶段:

  1. 编译模板本身,这时候主要检测语法错误,例如标点符号,变量名是否一致等。
  2. 当遇到使用模板时,会检测实参数目是否正确?参数类型是否匹配等。
  3. 在模板实例化的过程中,检测类型相关的错误。
引用折叠 和 右值移动

通常情况下我们不能定义引用的引用。但实际上,可以通过类型别名 和 模板类型参数来间接定义。

模板类型参数的替换实质上也是类型别名。

引用折叠就是在上面两种场景下发生,通常来说 T& & / T& && / T&& & 都会折叠成 T& ,而 T&& && 会折叠成 T&&

这个性质有个有趣的应用。意味着当用在模板函数实参如果为右值引用,则它可以接受任何实参,并且可以区分左右值。

当传递给它一个 int 左值,则T的类型会推断为 int & 此时该模板函数的形参为 int& &&,则引用折叠,T的类型为 int &

typedef int& IntOne;

int n = 23;
IntOne &x = n;      //变量类型为 int & 
IntOne &&y = n;		//变量类型为 int &

typedef int&& IntTwo;
IntTwo &z = n;		//变量类型为 int &
IntTwo &&w = 12;	//变量类型为 int &&
template<typename T>
void sfun(T &&n) { }


int main(){
	int n=0;
    int &r = n;
    sfun(n);	//T为 int &  引用折叠后,sfun中形参 n 的类型为 int&
	sfun(r);	//T为 int &  引用折叠后,sfun中形参 n 的类型为 int&
    sfun(12);	//T为 int && 引用折叠后,sfun中形参 n 的类型为 int&&
}

在实际中,这种引用折叠特性只用在两种场景右值引用用于两种情况,模板转发其实参 和 模板被重载。

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

其中remove_reference::type 是T类型的去引用类型。static_cast(t) 是将t转换成T 类型。

下来理解一下这个代码:

  • 当传给它一个左值,例如 int n;move(n);
    1. 则T 的类型推断为 int & 引用折叠后,t 的类型为 int &
    2. remove_reference<int &>::type 即为 int ,所以返回类型即为 int &&
    3. static_cast<int &&>( t ) 将t 从 int & 转化为 int &&
    4. 返回 int && 类型
  • 当传给它一个右值,move(10)
    1. 则T 的类型推断为 int ,t 的类型为 int &&
    2. remove_reference<int &&>::type 即为 int ,所以返回类型即为 int &&
    3. static_cast<int &&>( t ) 将t 从 int && 转化为 int &&
    4. 返回 int && 类型
转发

某些函数需要将一个或多个实参类型不变的转给其它函数,以往我们直接调用另一个函数往往会丢失掉const 或左右值的属性。

调用中使用 std::forward<T>() 在转发时可以保持类型信息

可变参数包

总的来讲可变参数分为两种,模板参数包和函数参数包,只在模板中有效,对于普通函数不能使用。

template<typename T>
void print(const T&a) {
	cout << a << " ";
}
template<typename T,typename ...Ags>	//Ags 是模板参数包
void print(const T& a,const  Ags&...rest) {	//rest 是函数参数包
	cout << a << ' ';
	print(rest...);
    sizeof...(rest); //获得保重元素个数
}
int main(){
    print("ac", 12, 34, "end");
}

模板与重载

函数模板可以和另一个函数模板或者普通函数发生重载。如果涉及重载,则函数匹配规则会发生改变。

几个简单的规则,转换优先,看谁转换少,同等条件转换次数一样,选不通用版本,如果存在普通函数,则选普通函数。

template<typename T>
void sfun(const T&x) {
	cout << "const T&" << endl;
}
template<typename T>
void sfun(T*x) {
	cout << "T*" << endl;
}
#if 0
void sfun(int *x) {
	cout << "int *" << endl;
}
void sfun(const int *x) {
	cout << "int *" << endl;
}
#endif
int main( ) {
	sfun(n);	//正常的匹配,到const T&
	sfun(&n);	//因为第一个需要int * 到 const int * 的转化,而第二个不需要.所以第二个
	sfun(( const int * ) &n);//此时模板1和模板2都不需要转化,但模板二更加特例化,只能接受指针,所以选模板2
}

模板元编程

人们发现模板的机制自身是一个完整的图灵机,理论上可以计算任何类型的值,因此有了模板元编程,它可以创造出在c++ 编译期内执行的程序。

本节只是介绍一个简单概念,感兴趣请自行搜索资料。

简单概念
  • 显式接口和运行期多态

    这几个概念与隐式接口和编译期多态 是相对应的,我们通过几个例子来理解。

    class Base{
    public:
    	virtual void show(){cout<<"Base"<<endl;}
    };
    class Derived:public Base{
    public:
        virtual void show(){cout<<"Derived"<<endl;}
    }
    void fun(Base & a){
    	a.show();
    }
    

    其中a对象的show 方法就是一个显式接口,也就是它在源码中可见,而运行期多态是通过动态绑定实现。

    在模板和泛型编程,反倒是隐式接口和编译期多态重要性更强一点。

  • 隐式接口与编译期多态

    如果将上面的例子更改一下;

    template<typename T>
    void fun(T& a){
    	a.show();
    }
    

    现在,a 必须支持的接口称为隐式接口,它是对所有调用fun 都必须支持的接口。

    根据不同类型的类型参数将会在编译期模板函数的实例化,使得调用得以成功,不同的类型参数会导致调用不同的函数(本例中 show),这称为编译期多态。

运行期多态和编译期多态的另一个区别在于:一个决定哪一个重载函数被调用,另一个决定哪一个虚函数被调用。

posted @ 2020-07-06 14:21  沉云  阅读(157)  评论(0编辑  收藏  举报