标准库算法都是函数模板,标准库容器都是类模板。
1. 定义模板
模板参数列表的类型名(T )前必须加上关键字class 或者typename 。
模板示例如下: 编译器会根据实参类型实例化出一个特定版本的函数。 T称为模板类型参数,可以将类型参数看作类型说明符,就像使用内置类型或者类类型说明符一样使用。可以用来指明返回值类型或者作为函数参数类型。
除了类型参数,还可以在模板中定义非类型参数,非类型参数表示值的类型参数。非类型参数可以是整形,指向对象或者函数的指针或左值引用。绑定到指针或引用的非类型参数的实参必须具有静态生存周期。 在使用过程时,编译器会使用传入的字面常量的长度替换M,N,实例化模板。 函数模板可以定义为内联的和常量表达式。在模板参数列表后,函数返回值之前用关键字inline和constexpr声明。 编写泛型代码有两个重要原则,为了保证类型无关性和可移植性: 1.将参数声明为const (保证不会拷贝,因为有的类型不能拷贝)。 2.函数体的比较条件仅使用<比较运算,可使用 less <T> 来满足只用< 的要求(降低了对类型需要重载的运算符的要求)。
1.1 模板提供者的责任
对于常规的函数和类,当调用函数时,编译器只需掌握函数的声明,对函数的定义没有要求;当使用类类型的对象时,编译器需要掌握类定义,而成员函数的定义则没有要求。 所以通常把函数声明和定义,类定义和类成员函数定义分开存放于源文件和头文件。
对于模板函数和类不同,因为编译期间需要生成对应的实例化版本,所以编译器需要掌握函数定义和类成员函数的定义。也就是说: 所以在实现时:
1.2 模板调用者的责任
模板错误三阶段: 模板本身有语法错误。 编译器使用模板时传参错误(类型、数目)。 模板实例化时类型错误,比如传进来的实参无法完成模板内的某些操作。
1.3 类模板
与函数模板不同的时,编译器无法为类模板推测模板参数类型,实例化类模板必需在模板名后的尖括号内提供额外信息。 但是当在类内部使用模板名时,无需提供额外的参数。 在模板外定义成员函数时,要注意只有在类名之后的部分才是在类的作用域之内,所以如果函数返回值需要用到类名,仍需要提供额外参数:
实例化一个类模板会重写该模板,且不同的重写后的类之间没有关联: 一个实例化的模板总是包含模板参数的,类模板中引用模板,调用者需要将自己的模板参数当作被调用模板的实参,如下,shared_ptr和vector都是模板,share_ptr调用vector模板:
1.3.1 类模板成员函数
类模板中成员函数的定义可以在模板内(隐式声明内联函数)也可以在模板外(需要以template开始,后接模板参数列表)。
类内声明: 类外定义: 类的成员只有在被用到时才会实例化,所以即使某种类型不支持某个操作,只要不用该特定操作,仍然可以用该类型实例化类。
1.3.2 类模板和友元
如果类声明了友元或者被声明为友元,由于类模板可以实例化,那么实例化后的类和模板的友元之间是什么关系? 取决于友元实参的传递。如果一方是模板一方不是模板,那么非模板的类和模板类的所以实例都是友元。如果双方都是模板,则根据友元声明的规则确定哪些实例和哪些实例是友元。
如果某个模板/非模板类的所有实例都是声明者的友元,则无需在声明友元之前声明该模板(称为前置声明)。
可将模板自己的类型参数声明为友元:
1.3.2.1 一对一友好关系
和模板友元的某个特殊的实例建立友元关系。模板的实例就是类。
1.3.2.2 通用和特定模板的友好关系
特定模板: 前面的一对一是只要实参相同都有友元,现在需要将条件缩小,只有当实参满足特定情况时才是友元: 通用模板,即在友元声明时使用和类模板本身不同的模板参数,此时所有模板实例都是声明者的友元:
1.3.3 模板类型别名
typedef可以引用模板实例类型,但是不能对模板这样做。 如下是正确的: 使用using可以为模板起一个别名,前面是模板标识template以及模板类型参数,后面是using操作,使用 别名+类型参数 的方法调用:
1.3.4 类模板的静态成员
和非模板不同的是,每个模板类的实例都有自己的一份static对象,并且静态成员以及静态成员函数也只有当使用时才会实例化。 和非模板相同,可以通过访问**类类型(也就是类模板的一个实例)**来访问一个类模板的static成员。
1.2 模板参数
模板参数的名字可以任取(前面的关键字可不能)。
模板参数的作用域和普通参数一样,但是模板参数名不能被重用,如下: 以及: 声明时的模板参数名不必与定义中的相同,但是类型和数量必须相同,这里的类型值得是参数是类型或非类型的。
类型成员和静态成员 都可以通过 类名+:: 访问。对与一般类,由于编译器清楚类的定义,可以清楚的知道访问的是类名还是静态成员。 但是对于模板类,可能会导致问题: 由于类型参数到运行时才指定,所以不知道传入的T::size_type 访问的类类型成员还是静态成员。 如果是前者,这条语句就是定义了一个类型为T::size_type的指针p。 如果是后者,这条语句就是将一个类型为T的变量size_type与p相乘。
默认情况下,C++假定作用域运算符访问的不是类型,所以默认为后者的情况。如果想要指定为前者情况,在传入模板参数时可以指定T为typename 。如下: 函数模板和类模板都可以有默认实参。 类模板使用默认实参在实例化时可以省略掉尖括号的对应部分。如下:
1.3 成员模板
普通类和模板类都可以包含本身是模板的成员函数,这种成员被称为成员模板。成员模板不能是虚函数。
如果类和成员函数都是模板,那么要想实例化成员函数,需要同时提供类和函数模板的实参。例子:
1.4 控制实例化
当多个源文件使用了相同的模板以及模板参数,那么每个文件都会有一个模板实例。大系统中这些开销会很严重。
如果已知实例化参数,可用extern显式实例化(显式是指无论后面有没有调用到,都会实例化模板,抑制了编译器自动实例化的行为)方法避免这些开销,extern意思是在程序的其他地方有非extern的声明和定义:
当遇到extern模板声明时,实例化的过程不会发生在本文件,而是发生在本声明对应的非extern声明所在的文件: 而另一个文件含有如下语句: 编译这个程序时,需要将如上两个文件链接到一起。
可以有多个extern声明,但只能有一个定义,这个定义就是实例化真正发生的地方,该定义会实例化类模板的所有成员,因为编译器不知道程序到底会用哪个,所以只能全实例化。 所以:
1.5 效率与灵活性
shared_ptr和unique_ptr的删除器的区别: shared_ptr允许轻易重载删除器,对指针本身无需做更改。 unique_ptr的删除器类型是指针类型的一部分,修改了删除器,需要在实例化unique_ptr时在实参显式将删除器指定为模板实参。
1.5.1 shared_ptr,运行时绑定删除器
shared_ptr不直接将删除器保存为成员,因为删除器可以运行时动态改变,类成员类型是改不了的。
shared_ptr的删除器以指针或者是封装了指针的类实现。假设删除器指针为del,可用如下逻辑实现析构函数: 如果del为空,即没指定删除器,则直接调用标准库的销毁函数,否则用del完成销毁。
1.5.2 unique_ptr,编译时绑定删除器
unique_ptr将删除器保存为类成员,假设删除器指针为del,可用如下逻辑实现析构函数: 总而言之,编译时绑定删除器避免了运行时绑定的开销,而运行时绑定删除器更加方便。
2. 模板实参推断
通过调用函数传递的实参类型推断出函数模板实参类型的过程称为模板实参推断。
2.1 类型转换与模板类型参数
当函数形参使用了模板参数,如下情况: 就可能出现类型转换。 这些转换是有限的,只有两种情况能转:
同一个模板类型参数可能被传给多个函数形参,这时需要保证这些参数类型相同,假设compare的形参都是使用的同一个模板类型参数。 要是需要给不同形参不同类型的实参,不要用同一个模板类型参数: 对于正常类型的参数,即不使用模板类型参数的参数,可以和正常传参一样进行类型转换。
2.2 函数模板显式形参
当编译器无法推断出模板实参类型(返回值类型没有出现在函数形参中)或者想要用户控制模板实例化时,可以显式指定模板实参。
实例化时可显式指定返回值类型,放在<> 内,从左至右依次和模板类型参数匹配,剩余的模板类型参数从实参类型推断,注意是指定模板类型参数的类型,不是调用的函数的形参类型: T2,T3可以通过传入的实参推断出类型,T1没办法推断,只能指定。
即使是普通类型的实参也可以用该方法显式转化类型:
2.3 尾置返回类型和类型转换
尾置返回出现在参数列表之后,可以利用参数类型推导返回值类型: 为了获取引用对象的类型,可使用标准库的类型转换模板,定义在type_traits头文件中。比如remove_reference模板,它有一个模板类型参数,和一个名为type的类型成员。接收一个引用类型,返回它所指向的对象的类型。
2.4 函数指针和实参推断
当使用函数模板给函数指针赋值时,根据函数指针所指向函数的参数类型实例化函数模板。如果通过函数指针不能确定模板实参的唯一类型,则会报错,如下: 模板定义: 使用compare初始化pf1,同时函数指针pf1实例化compare: func参数无法唯一实例化模板: 可以通过显式模板实参消除歧义:
2.5 模板实参的推断与引用
当函数参数是对模板实参的普通引用时,只能传递左值,但是const和非const都可以,因为T是个类型,本身就可以加const: 当函数参数是对模板实参的常量引用时,可以传递任何对象,(const和非const,临时对象,字面值都可以,因为常量引用可以绑定到右值): 当函数参数是对模板实参的右值引用时,只能传递右值,且T类型就是右值对应的类型,这也是唯一一种能真实反映实参类型的引用: 通常情况下,不能将右值引用绑定到左值上,但是C++定义了两个额外规则,使得这种绑定成为可能:
1.当右值引用指向的是模板类型参数,可以将左值传递给函数的右值引用参数,此时编译器会推断模板类型参数是左值引用类型。这个规则也就导致了: 2.可间接创建引用的引用,引起引用折叠。引用一旦折叠,会折叠成一个普通的左值引用。只有一种情况,右值引用的右值引用会折叠为右值引用。
这个规则也是std::move能实现的根源。 函数模板使用右值一般只用于模板转发其实参或者被重载。重载好说,转发实参后面会说,总之是利用了右值作为模板类型参数能真实反映实参类型的特性。如果使用左值,无论是const还是非const,得到的T的类型都是参数去引用后的类型。
不过,无论是左值还是右值,都能保留参数的const属性,因为引用的const就是底层const,不能忽略。
使用右值引用的函数模板的重载: 与非模板函数重载一样,第一个版本绑定到可修改右值,第二个版本绑定到左值和不可修改右值。
2.6 理解std::move
下面为move的实现: 现在解释move既能接收左值又能接收右值。考虑如下语句:
当传入的参数是左值时: 最终将move实例化为: 将左值绑定到右值引用,正是我们想要的。
当传入参数是右值时:
最终将move实例化为: 将右值绑定到右值引用,也是我们想要的。
另外,函数体中也用了一条针对右值引用的特许规则:虽不能把左值隐式转化为右值引用,但是可以使用static_cast 显式完成转换。
2.7 转发
某些函数需要将参数原封不动的转发给其他函数。
一般情况下直接把参数写进去进行,但是当被调用函数需要用引用作为参数时,被调用函数对该引用参数的修改不会影响到调用者接收的实参。 如下,调用者flip,被调用者f: 究其原因在于参数传给flip时是传的副本。
2.7.1 定义可保持类型信息的函数参数
将函数参数定义为模板类型参数的右值引用可以完整保留参数类型信息。 虽然参数保留了类型信息,但是当再传给下一层函数时,又会出问题:因为函数参数和其他任何变量一样,都是左值,所以即使给外层函数传递一个右值,在传递给被调用函数时,也会被转化为一个左值。
为了解决这个问题,可以使用std::forward 保持类型信息。 std::forward 和std::move 类似,都是返回右值引用。但是std::forward必须通过显式模板实参来调用。如下例: 用std::forward 和右值引用即可保留参数类型信息,无论传递的什么参数。
3. 重载与模板
多个重载的模板和重载的函数一样,有候选集: ①对一个调用,其候选函数包含所有模板实参推断成功的函数模板实例(注意是模板实例参与候选不是模板参与候选),也就是说,候选集中的模板总是可行的。 ②可行函数按类型转换来排序。 ③如果某个函数匹配最佳,则选择它。如果有多个同样好的函数: -----只有一个非模板,选择它。 -----只有函数模板,有一个模板比其他模板更特例化,选择它。(特例化就是指参数可选范围最少的) 否则有歧义。
4. 可变参数模板
可变参数模板就是接受可变数目参数的模板函数或者模板类。这些可变数目的参数被称为参数包。根据参数属于谁,可分为模板参数包和函数参数包。
模板包,用class... 和typename... 表示接下来的参数表示零个或多个类型的列表。 函数参数包,**类型名后跟…**表示零个或多个给定类型的非类型参数列表。 示例如下: 可用sizeof... 求出包内元素数目。
4.1 编写可变参数函数模板
initializer_list 可用来定义一个接收可变数目实参的函数,但是这要求所有的实参具有相同的类型。
可变参数在实际使用中常与递归结合: 如果参数包为空,调用哪一个函数?有可变参数的还是没有可变参数的? 答案是没可变参数的。因为他更特例化。这也保证了print不会无限调用自生而导致死循环。
4.2 包扩展
对一个声明好的包,使用... 对包扩展。... 既能标识一个参数包,同时也能有扩展该包的功能。 扩展时,可指定为每个元素要应用的模式: 第一个扩展是对Args的扩展,以模式 const 构成元素& 解包。扩展出**类型(因为构成元素就是类型)的列表,由逗号分隔。 第二个扩展是对rest,以模式 构成元素 解包。扩展出由变量名(包中元素)**组成的,逗号分隔的列表。
比如: 实例化为: 内部调用为: 模式可以是定义为可调用对象的形式,这种情况会对参数包内的每个元素调用该函数。注意扩展包符号的位置: 前者为对rest的每个元素按debug_rep()的模式展开。 后者为将rest展开后,把用逗号分隔的参数列表传给debug_rep。
4.3 转发参数包
前面介绍了如何保持类型的传递参数。即右值引用保存实参的类型信息,然后使用forward 将保存的实参类型信息完整的传给下一个函数。
将参数包声明为右值引用其实就是将扩展包时的模式设置为右值引用; 对参数使用std::forward即在扩展包时将模式设置为std::forward<>() 。 ... 会将模式表达式的所有函数包都按模式扩展包。
5. 模板特例化
当不能/不希望(通用模板对某些特定类型不合适,模板可能实例化失败)使用模板版本时,可以定义类或函数模板的一个特例化版本。
特例化模板就是将模板的一个或多个模板参数指定为特定类型。
5.1 特例化函数模板
特例化函数模板需要为模板的每一个模板参数都提供完全特例化实参,可用空尖括号表示,所提供的实参必须和先前声明模板的类型匹配。
例子,对如下函数模板: 它的一个特例化结果为: 特例化的参数类型为一个常量引用,和模板类型匹配。引用的内容是一个指向常量字符的指针。最终结果是一个引用,所引用的指针是常量的,指针所指向的字符也是常量的。
特例化的模板在函数匹配时归类于模板实例那一档,而不会因为它实例化了就当成非模板函数。
5.2 特例化类模板
以特例化hash为例: ==>1.需要一个重载的调用运算符。接收一个容器关键字类型的对象,返回一个size_t。 ==>2.默认构造函数和拷贝赋值运算符。 ==>3.两个类型成员,表示调用运算符的返回类型和参数类型。 如下: 在定义Sales_data 的函数调用运算符时,实际上调用了标准库为其他类型特例化的hash类版本。本次特例化也是将一个Sales_data 的特例化版本交给了标准库(打开命名空间再定义),之后遇到hash<Sale_data> 时就会自动使用这个版本。
于特例化函数模板不同,特例化类模板并不需要为每个类型参数提供实参,还有,特例化类模板不需要指定参数的全部特性,可以只指定部分特性。也就是说,特例化的类模板本身还可能是类模板。
部分特例化的模板参数是原始模板的参数列表的一个子集或是一个特例化版本。
特例化例子: 注意类模板没有模板实参推断,需要为模板提供额外参数才能按照参数实例化类。
5.2.1 特例化成员而不是类
可以不特例化整个类,而是只细化下某个成员的类型。 之后如果实例化该成员,得到的是这个特例化的版本:
注意区分显式实例化、模板特例化、显式形参三者。 模板特例化是重定义,特例化后其函数体/类成员不要求和原来的模板一样,可以视为新的函数或者类。但是还是属于模板实例化这一类,在函数匹配时把他当成某个模板实例化的结果。
实例化只是显式的生成实例,无论用没用到该函数。对函数内容没有改变。
|