Chapter16:模板

  • 函数模板

编译器通常用函数实参来为我们推断模板实参。

编译器用推断出的模板参数来为我们实例化(instantiate)一个特定版本的函数。生成的版本通常被称为模板的实例。

我们可以在模板中定义非类型参数(nontype parameter)。一个非类型参数表示一个值而非一个类型。模板实参必须是常量表达式,从而允许编译器在编译时实例化模板。(绑定到非类型整数参数的实参必须是常量表达式;绑定到指针或引用非类型参数的实参必须具有静态的生存期;我们不能用一个普通的局部变量或动态对象作为指针或引用非参数模板参数的实参。)

inline或constexpr的函数模板:

1 //正确
2 template<typename>
3 inline T min(const T&, const T&);
4 //错误
5 inline template<typename>T min(const T&, const T&);

模板编译

当编译器遇到一个模板定义时,它并不生成代码。只有当实例化一个模板时,编译器才生成代码。

当我们使用模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。

通常来说,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类的定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放到源文件中;

模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。

  • 类模板

与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。

为了阅读模板类代码,应该记住类模板的名字不是一个类型名。

对于一个实例化了的类模板,成员函数只有在被用到时才进行实例化。

当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。当我们在类模板外定义其成员时,必须记住,我们并不在类的作用域中,指导遇到类名才表示进入类的作用域:

 1 template<typename T> class BlobPtr
 2 {
 3     BlobPtr& operator++();//不需要模板参数
 4 };
 5
 6 template<typename T>
 7 BlobPtr<T> BlobPtr<T>::operator++(int)//需要模板参数
 8 {
 9     BlobPtr ret = *this; //不需要模板参数
10     ++*this;
11     return ret;
12 }

类模板和友元:

情况一:一对一

 1 //为了引用模板的一个特定实例,我们必须首先声明模板自身
 2 template<typename> class BlobPtr;
 3 template<typename> class Blob;
 4 template<typename T> bool operator==(const Blob<T>&, const Blob<T>&);
 5
 6 template<typename T> class Blob
 7 {
 8     friend class BlobPtr<T>;
 9     friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
10
11 };

情况二:

 1 //因为只需要Pal的特定实例;Pal2所有实例都需要,所以只有Pal前置声明
 2 template<typename T> class Pal;
 3 class C
 4 {
 5     friend class Pal<C>;//Pal<C>是友元
 6     template<typename T> friend class Pal2;//Pal2所有实例都是C的友元
 7 };
 8
 9 template<typename T>class C2
10 {
11     friend class Pal<T>;
12     template<typename X> friend class Pal2;
13     //Pal3是一个非模板类,它是C2所有实例的友元
14     friend class Pal3;
15
16 };

情况三:新标准中可以将模板类型参数声明为友元

1 template<typename Type> class Bar
2 {
3     friend Type;
4 };
  • 模板参数

一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。与任何其它的名字一样,模板参数会隐藏外层作用域中声明的相同名字。

一个问题:我们使用作用域运算符(::)来访问static成员和类型成员,在普通代码中,编译器掌握类的定义,所以可以知道要访问的名字是类型还是static成员;但是对于模板代码,T::mem,编译器不知道mem是一个类型还是数据成员,所以必须显式指定。

默认情况下,C++假定通过作用域运算符访问的名字不是类型,如果要访问类型,需要typename显式指定,那么:

1 template<typename T>
2 typename T::value_type top()
3 {
4 }
  • 控制实例化

当模板被使用时才会进行实例化这一特性意味着,相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中都会有该模板的一个实例。

在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显示实例化来避免这种开销。

1 extern template declaration;//实例化声明
2 template declaration;//实例化定义
1 extern template class Blob<string>;//声明
2 template int compare(const int&, const int&);//定义

当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序的其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。

由于编译器在使用一个模板时自动化对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前。

一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。因为编译器不知道程序会使用哪些成员函数。

  • 模板实参推断

只允许两种转换:

1. const转换:可以将一个非const对象的引用传递给一个const的引用形参;

2. 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。

其他类型转换,如算术转换、派生类向基类转换,以及用户定义的转换,都不能应用于函数模板。

但是如果函数参数类型不是模板参数,则对实参进行正常的类型转换。

同时,对于模板类型参数已经显式指定了的函数实参,也可以进行正常的类型转换。

对于这种情况怎么办?

1 template<typename It>
2 ? ? ? &fcn(It beg, It end)
3 {
4     return *beg;
5 }
1 template<typename It>
2 auto fcn(It beg, It end) ->decltype(*beg)
3 {
4     return *beg;
5 }
  • 模板实参推断和引用
 1 template<typename T> void f1(T&);
 2 f1(i);//i是int,T推断为int
 3 f1(ci);//ci是const int,T为int
 4 f1(5);//错误,传递给&参数的必须是左值,(因为const可以转换,所以不能推断出const来)
 5
 6 template<typename T> void f2(T&);
 7 f2(i);//i是int,T推断为int
 8 f2(ci);//ci是const int,T为int
 9 f2(5);//T为int
10
11 template<typename T> void f3(T&&);
12 f3(42);//T为int

f3(i)这样的调用时不合法的。但是在C++中,正常的绑定规则之外,有两个例外,这是move设施正确工作的基础。

1. 当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型;即:f3(i),T为int&。

2. 如果我们间接创建了引用的引用,那么除了右值引用的右值引用之外,其他都折叠为左值引用。

由此:我们可以把任意类型的实参传递给T&&类型的函数参数。

但是,当代吗涉及的类型可能是普通(非引用)类型,也可能是引用类型时,编写正确的代码就异常困难,所以,在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载

std::move

如何将左值转换为右值?可以使用static_cast显式地将一个左值转换为一个右值。但是,最好使用move

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

转发

转发需要保证实参的所有性质:实参类型是否是const以及实参是左值还是右值。

template<typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
    f(t2, t1);
}

void f(int v1, int& v2)
{
    cout << v1 << " " << ++v2 << endl;
}

flip1(f, j, 42);//T1 推断为int,不符合预期

如何解决?答案是使用右值引用。

template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
    f(t2, t1);
}

但是,还有一个问题,F函数如果参数是左值引用,可以工作的很好;如果是右值引用,则出现错误。

void g(int &&v1, int& v2)
{
    cout << v1 << " " << ++v2 << endl;
}flip2(g,i,42);//t2是类型为右值引用的变量,作为左值,不能传递给右值引用

如何解决这一问题?forwaed<T>返回类型是T&&

1 template<typename F, typename T1, typename T2>
2 void flip(F f, T1 &&t1, T2 &&t2)
3 {
4     f(std::forward<T2>(t2), std::forward<T1>(t1));
5 }

如果实参是右值,那么Type就是普通类型,forward就返回Type&&,还是右值;

如果实参是左值,那么Type就是左值引用类型,forward就返回左值引用的右值引用,折叠之后是左值引用。

所以:右值引用形参+forward完成了转发工作

  • 模板重载的几条规则

如果同样好的函数中只有一个是非模板函数,则选择此函数;

如果同样好的函数中没有非模板参数,而有很多函数模板,且其中一个模板比其他模板更特例化,则选择此模板。(没有他的话,本质上(const T&)可以用于任何类型);

  • 可变参数模板:

参数包:模板参数包和函数参数包;

当我们需要知道包中有多少元素时,可以使用sizeof...()运算符。

可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后用剩余的实参调用本身。

template<typename T>
osteram &print(ostream &os, const T &t)//终止函数
{
    return os << t;
}
template<typename T,typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
    os << t << ",";
    return print(os, rest...);
}

包扩展:模式。

一个应用:转发参数包

class StrVec
{
public:
    template<class...Args>void emplace_back(Args&&...)
    {
        alloc.construct(first_free++, std::forward<Args>(args)...);
    }
};
  • 模板特例化

在某些情况下,通用模板的定义是不合适的。需要特例化。

函数模板特例化

template<>
int compare(const char* const &p1, const char* const &p2)
{
    return strcmp(p1, p2);
}

注意:一个特例化版本本质上是一个实例,而非函数的重载版本,因此,不影响函数匹配

类模板可以部分特例化:只指定部分参数;或者参数的部分特性(例如引用)。

可以特例化成员而不是类。

时间: 2024-10-12 18:41:44

Chapter16:模板的相关文章

Vue.js项目模板搭建

前言 从今年(2017年)年初起,我们团队开始引入「Vue.js」开发移动端的产品.作为团队的领头人,我的首要任务就是设计 整体的架构 .一个良好的架构必定是具备丰富的开发经验后才能搭建出来的.虽然我有多年的前端开发经验,但就「Vue.js」来说,仍然是个新手.所幸「Vue.js」有一个配套工具「Vue-CLI」,它提供了一些比较成熟的项目模板,很大程度上降低了上手的难度.然而,很多具体的问题还是要自己思考和解决的. 项目划分 我们公司的H5产品大部分是嵌套在手机客户端里面的页面.每个项目的功能

ac自动机基础模板(hdu2222)

In the modern time, Search engine came into the life of everybody like Google, Baidu, etc. Wiskey also wants to bring this feature to his image retrieval system. Every image have a long description, when users type some keywords to find the image, th

hdu 2966 In case of failure kdtree模板题

问求每个点距离平方的最小的点 kd-tree模板题…… 1 #include<bits/stdc++.h> 2 #define cl(a,b) memset(a,b,sizeof(a)) 3 #define debug(x) cerr<<#x<<"=="<<(x)<<endl 4 using namespace std; 5 typedef long long ll; 6 typedef pair<int,int>

eclipse添加xml模板

//因为学javaee,中框架,,感觉配置文件好多, window-preferences-xml-xmlfiles-editor-templates-选中模板,-edit

POJ3528 HDU3662 三维凸包模板

POJ3528 HDU3662 第一道题 给定若干点 求凸包的表面积,第二题 给定若干点就凸包的面数. 简单说一下三维凸包的求法,首先对于4个点假设不共面,确定了唯一四面体,对于一个新的点,若它不在四面体内,为了让它进入凸包, 则对于所有凸包上的边,若边的一面是该点可以看到的而另一面看不到,则该点与该边构成的面要加入凸包. 模板代码非常清晰, #include<stdio.h> #include<algorithm> #include<string.h> #includ

zabbix用自带的模板监控mysql

先看一下zabbix自带的mysql模板监控项: #很少是吧,没事生产环境一般我们不用,下一篇将介绍生产环境用的另一种mysql监控. 配置zabbix自带的模板监控mysql数据库:

小程序砸金蛋、外卖模板上线啦,快到酷客多商户后台更新!

最近,微信小程序官方发文不断,又开放十几项接口,逐步给企业主带来跟多福利.于此同时,酷客多研发团队也保持着一贯的研发和版本迭代速度,此次版本主要新增幸运砸金蛋.外卖模板.意见反馈三个模块 1.新增幸运砸金蛋,大奖中不停 通过此功能可增加平台趣味性,增强用户粘性,刺激用户二次消费,是与用户互动的一大利器. 2新增外卖模板,外卖送起来 此模板是餐饮企业的福利,可在注册或者酷客多商户管理后台直接选择此模板,瞬间让您的小程序首页变的高大上,从此再也不用担心第三方外卖平台高额的佣金和账期了,因为酷客多只提

C++学习笔记50:队列类模板

队列是只能向一端添加元素,从另一端删除元素的线性群体 循环队列 在想象中将数组弯曲成环形,元素出队时,后继元素不移动,每当队尾达到数组最后一个元素时,便再回到数组开头. 队列类模板 //Queue.h #ifndef QUEUE_H #define QUEUE_H #include <cassert> //类模板的定义 template <class T, int SIZE = 50> class Queue { private: int front, rear, count; T

ReactJS React+Redux+Router+antDesign通用高效率开发模板,夜间模式为例

工作比较忙,一直没有时间总结下最近学习的一些东西,为了方便前端开发,我使用React+Redux+Router+antDesign总结了一个通用的模板,这个技术栈在前端开发者中是非常常见的. 总的来说,我这个工程十分便捷,对于初学者来说,可能包含到以下的一些知识点: 一.React-Router的使用 Router是为了方便管理组件的路径,它使用比较简单,一般定义如下就行,需要注意的是,react-router的版本有1.0-3.0,各个版本对应的API大致相似,但也有不同,我使用的是2.X的,