| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> 《C++Primer 第五版》——第十六章 模板与泛型编程 -> 正文阅读 |
|
[C++知识库]《C++Primer 第五版》——第十六章 模板与泛型编程 |
《C++Primer 第五版》——第十六章 模板与泛型编程面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:OOP能处理类型在程序运行之前都不知道的情况;而在泛型编程中,在编译时就能获知类型了。 16.1 定义模板① 当程序员显式提供了模板实参; 16.1.1 函数模板我们可以定义一个通用的 函数模板(function template) ,而不是为每个类型都定义一个新函数。一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。
Note:在模板定义中,模板参数列表不能为空。 实例化函数模板当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。即,当我们使用函数模板时,编译器根据实参的类型来推断绑定到模板参数的类型(即类型实参)。
编译器用推断出的模板实参来 实例化(instantiate) 一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参替换对应的模板参数来创建出模板的一个新“实例”。
类型模板形参模板函数有一个或多个类型模板形参(template type parameter),一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型模板形参可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
非类型模板参数除了定义类型参数,还可以在模板定义中定义非类型参数(nontype parameter)。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字 class 或 typename 来指定非类型参数。
inline 和 constextpr 的函数模板函数模板同样可以声明为 inline 或 constexpr 的,和非模板函数一样。关键字 inline 或 constexpr 说明符放在模板参数列表之后,返回类型之前:
编写类型无关的代码编写泛型代码有两个重要原则需要程序员遵循:
那为什么函数参数要是对 const 的引用?
那为什么仅使用
实际上,如果真正关心类型独立性和可移植性,可能需要使用标准库函数对象 less (参见14.8.2节)来定义函数。
原始版本的问题是:如果用户调用它比较两个指向不同数组的指针,那么指针的比较行为是未定义的。 Note:模板程序应该尽量减少对实参类型的要求。 模板编译当编译器遇到一个模板的定义时,它不会产生代码。只有当实例化模板的一个特定版本时,编译器才会产生代码。当我们使用(而不是定义)模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到的时间。 Note:函数模板和类模板成员函数的定义通常放在头文件中。
大多数编译错误在实例化期间报告在实例化模板时,编译器通常可能会在三个阶段标记错误:
当编写模板时,代码不是特定于类型的,但是模板代码通常会对要使用的类型做出一些假设。
WARNING: 16.1.2 类模板类模板(class template) 是用于生成类的蓝图。与函数模板不同的是,编译器不能为类模板根据模板实参,来推断模板参数类型。 定义类模板类似函数模板,类模板的声明和定义以关键字 template 开始,后紧跟模板形参列表。在类模板(及其成员)的定义中,我们将模板形参当作替身,替代使用模板时用户需要提供的类型或值。
实例化类模板当使用一个类模板时,我们必须提供额外信息——即, 显式模板实参(explicit template argument) 列表,它们被绑定到模板形参。编译器使用这些实参来实例化特定的类。
Note:
在模板作用域中引用模板类型为了阅读模板类代码,应该记住类模板的名字不是一个类型名。类模板是用来实例化类型的,而一个实例化的类型总是包含模板实参的。
类模板的成员函数????与其它类相同,我们既可以在类模板内部,也可以在类模板外部为其定义函数,且定义在类模板内的成员函数被隐式声明为内联函数。
Blob的构造函数???同样的,类模板的构造函数在类外的定义和普通函数一样,要以 template 关键字开始:
类模板实例的成员函数、成员类和 static 数据成员的实例化???默认情况下, 一个类模板的某个实例 的成员类、 static 数据成员和成员函数(无论是否是函数模板),通常只有当程序用到它时才进行实例化。 除非该类模板的实例是显式实例化的 例如,下面代码:
???如果一个类模板的某个实例的成员类、成员函数或 static 数据成员没有被使用,则它通常不会被实例化。 ???成员类、成员函数和 static 数据成员只有在被用到时才进行实例化,这一特性使得——即使某种类型不能完全符合模板操作的要求(比如被该类型实例化的成员函数只声明未定义),我们仍然能用该类型实例化类。 在类代码内简化模板类名的使用???一般当我们使用一个类模板类型时必须提供模板参数,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不需要提供模板实参:
???可以发现,BlobPtr 的前置递增和递减返回的是 在类模板外使用类模板名???当我们在类模板外定义其成员时,必须记住,我们不在类的作用域中,直至遇到类模板名才表示进入类模板的作用域:
???由于返回类型位于类的作用域外,所以必须指出返回类型是一个实例化的 BlobPtr ,它所用的类型和类实例化所用类型一致。在函数体内的时候,我们已经进入了类的作用域,因此在定义 ret 时无需提供模板实参,编译器将假定我们使用的类型与用户实例化所用的类型一致。因此 ret 的定义与下面代码等价:
类模板和友元???当一个类包含一个友元声明时,类与友元各自是否是模版是无关的。
一对一的友元关系???类模板与另一个模板间友好关系的最常见的形式是:建立对应实例及其友元间的友好关系。 ???一个模板声明包括模板形参列表:
???友元的声明用 Blob 的模板形参作为它们自己的模板实参。因此,友元关系被限定在用相同类型实例化的 Blob 和 BlobPtr 相等运算符之间 :
通用和特定的模板友元关系????一个类也可以将另一个模板的每个实例都声明为自己的友元,或者指定特定的实例为友元:
???为了让某个模板的所有实例成为友元,友元声明中必须使用与类模板本身形参不同的模板参数。 通用和特定模板的友元关系需要注意一点:
令模板自己的类型参数成为友元???在C++11中,我们可以将模板的类型参数声明为友元:
???此处我们将用来实例化 Bar 的类型声明为友元。即对于某个类型名 S ,它将会成为 ???值得注意的是,虽然友元通常来说是一个类或函数,但我们完全可以用内置类型来实例化类模板,这种与内置类型的友好关系是允许的,以便我们能用内置类型来实例化类模板。 模板类型别名???类模板的一个实例定义了一个类类型。与任何其他类类型一样,我们可以用关键字 typedef 定义一个类型别名来引用被实例化的类模板,比如 string 就是
???由于(类或函数)模板不是一个类类型或函数,我们不能用关键字 typedef 定义一个类型别名来引用一个类模板。即,无法定义一个 typedef 引用
???当我们用 using 定义一个模板类型别名时,可以使用,可以用具体的类型或值,来固定一个或多个模板参数:
类模板的 static 成员???与任何其它类相同,类模板可以声明 static 成员:
???在这段代码中, Foo 是一个类模板,它有一个名为 count 的 public static 成员函数和一个名为 ctr 的 private static 数据成员。每个 Foo 的实例都有其自己的 static 成员实例。即,对任意的给定类型 X,都有一个
???与任何其它 static 数据成员相同,模板类的每个 static 数据成员必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的 static 对象,它被该实例的全部对象共享。
???定义的开始部分是模板的参数列表,随后是我们定义的成员的类型和名字。与往常一样,成员名包括成员所属的类名,对于从模板实例化的类,类名还包括模板实参。因此,当使用一个特定的模板参数类型实例化 Foo 时,将会为该类类型实例化一个独立的 ctr ,并将其初始化为 0。 ???与非模板类的静态成员相同,我们既可以通过类类型对象来访问一个类模板的 static 成员,也可以使用作用域运算符直接访问成员。当然,为了通过类名来直接访问 static 成员,我们必须引用一个特定的实例:
???类似类模板的其他成员函数,一个类模板的 static 成员只有在使用时才会实例化。 16.1.3 模板参数???类似函数参数的名字,一个模板参数的名字也没有什么内在含义。我们通常将类型参数命名 T,但实际上我们可以使用任何名字:
模板参数与作用域???模板参数 遵循 普通的作用域规则。
???由于模板参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次:
模板声明???模板声明必须包含模板参数:
???与函数参数类似,声明中的模板参数的名字不必与定义中的相同:
???当然,一个给定模板的每个声明和对应定义,必须有相同数量和种类(即,类型或非类型)的参数。
使用类的类型成员( typename 和 class 在模板中的唯一区别)???在普通(非模板)代码中,编译器知道类的定义。编译器知道通过作用域运算符访问的名字是类型还是 static 成员。例如,string::size_type,编译器有 string 的定义,编译器知道 size_type 是一个类型而非成员。 ???但是对于模板代码就存在困难。例如,假定 T 是一个模板类型参数,当编译器遇到类似 T::mem 的代码时,它不会知道 mem 是一个类型成员还是 static 数据成员,直到实例化时才会知道。像 mem 这样的名字就被称为依赖名。 ????依赖名(dependent name):在模板(类模板和函数模板)定义中,某些构造的含义可以在不同的实例化间有所不同。特别是,类型和表达式 可以取决于 类型模板形参的类型和非类型模板形参的值。 ???但是,为了处理模板,编译器必须知道依赖名是否表示一个类型。例如,假定 T 是一个类型参数的名字,当编译器遇到如下形式的语句:
???只有当编译器知道 T::size_type 是类型成员时,上面语句才翻译为 p 是一个 T::size_type 类型的指针 ,否则编译器会理解为这是一个 T::size_type 中的 static 数据成员与 p 相乘。 ????默认情况下, C++ 假定通过作用域运算符访问的名字不是类型。 所以,我们需要使用 typename 来达到以上目的:
???当我们希望 告知编译器一个依赖名表示类型时,必须使用关键字 typename ,不能使用 class 。 默认模板实参???就像为函数参数提供默认实参一样,我们也可以在模板参数列表中提供默认模板实参(default template argument)。 ???例如,默认使用标准库的 less 函数对象版本来编写 compare :
在这段代码中,我们为模板添加了第二个类型参数 F ,表示可调用对象(参见10.3.2节)的类型;并定义了一个新的函数参数 f ,绑定到一个可调用对象上。 ???我们为此模板参数提供了默认实参,并为其对应的函数参数也提供了默认实参。默认模板实参指出 compare 将使用标准库的 less 函数对象类,它是使用与 compare 一样的类型参数实例化的。默认函数实参指出f将是类型F的一个默认初始化的对象。 ???当用户调用这个版本的 compare 时,可以提供自己的比较操作,但这并不是必需的:
第一个调用使用默认函数实参,即,类型 ???在第二个调用中,我们传递给 compare 三个实参: compareIsbn (参见11.2.2节)和两个 Sales_data 类型的对象。当传递给 compare 三个实参时,第三个实参的类型必须是一个可调用对象,该可调用对象的返回类型必须能转换为bool值,且接受的实参类型必须与compare的前两个实参的类型兼容。与往常一样,模板参数的类型从它们对应的函数实参推断而来。在此调用中, T 的类型被推断为 Sales_data , F 被推断为 compareIsbn 的类型。 ???与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以被提供默认实参。 模板默认实参与类模板???无论何时使用一个类模板,我们都必须在模板名之后接上尖括号
???此例中我们实例化了两个 Numbers 版本: average_precision 是用 int 代替 T 实例化得到的; lots_of_precision 是用 long double 代替 T 实例化而得到的。 16.1.4 成员模板???一个类 (普通类或类模板) 可以包含 本身是模板的函数或类 作为成员——这种模板被称为成员模板(member template)。 ???成员模板不能是虚函数。 ???成员模板的声明和定义也是以模板参数列表开始。 普通(非模板)类的成员模板???作为普通类包含成员模板的例子——我们定义一个类,类似 unique_ptr 所使用的默认删除器类型。类似默认删除器,我们的类将包含一个重载的函数调用运算符,它接受一个指针并对此指针执行 delete 。与默认删除器不同,我们的类还将在删除器被执行时打印一条信息。由于希望删除器适用于任何类型,我们将调用运算符定义为一个模板:
???与任何其它的模板相同,成员模板的声明和定义也是以模板参数列表开始。我们可以用这个类代替 delete 表达式:
???由于 DebugDelete 会 delete 给定的指针,我们也可以将 DebugDelete 作为 unique_ptr 的删除器。为了重载 unique_ptr 的删除器,我们在尖括号内给出删除器类型,并提供一个这种类型的对象给 unique_ptr 的构造函数:
???在本例中,我们声明 p 的删除器类型为 DebugDelete,并在 p 的构造函数中提供了该类型的一个未命名对象。 ???unique_ptr 的析构函数会调用 DebugDelete 的调用运算符。因此,无论何时 unique_ptr 的析构函数实例化时,DebugDelete 的调用运算符都会实例化:因此,上述定义会这样实例化:
类模板的成员模板???对于类模板,我们也可以为其定义成员模板。在此情况下,类模板和成员模板各自拥有自己的、独立的模板参数。 ????例如,我们为一个类模板 Blob 添加一个构造函数,它接受两个迭代器,表示要拷贝的元素范围。由于我们希望支持不同类型序列的迭代器,因此将构造函数定义为模板:
????与类模板的普通成员不同,成员模板是模板。所以,当我们在类外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的模板参数列表在前,后紧跟成员自己的模板参数列表,然后就是 类名::成员 :
???因为普通的作用域规则,在第一段代码中,因为在类模板的作用域中,所以可以省略那部分。 实例化与成员模板???为了实例化一个类模板的成员模板,我们必须同时提供类模板和成员模板的实参。
16.1.5 显式(控制)实例化???模板本身不是类型、对象或任何其它实体,所以不会只从仅含模板的定义的源文件中生成任何代码。 ???一般只有当模板被使用时才会进行实例化,这一特性意味着,相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。 ???在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在 C++11 中,我们可以通过 显式实例化(explicit instantiation) 来避免这种开销。 ???模板的显式实例化有以下形式(声明和定义):
???declaration 是一个类模板或函数模板声明,其中所有模板参数也都被替换为模板实参。例如:
???当编译器遇到 extern 模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为 extern 就表示承诺在程序其他位置有该实例化的一个非 extern 声明(定义)。对于一个给定的实例化版本,可能有多个 extern 声明,但必须只有一个定义。 ???由于编译器在使用一个模板时自动对其实例化,因此 extern 声明必须出现在任何使用此实例化版本的代码之前:
???我们在 templateBuild.cc 中实例化上述模板,并定义:
???当编译器遇到一个实例化定义(与声明相对),它为其生成实例化的代码。因此,文件 templateBuild.o 将会包含 compare 的 int 实例化版本的定义和 Blob 类的定义。 当我们编译此应用程序时, 必须将 templateBuild.o 和 Application.o 链接到一起。 Note:对每一个实例化声明,在程序中某个位置必须由其显式的实例化定义。 模板的实例化定义会实例化其所有成员???一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。当编译器遇到一个实例化定义时,它不了解程序使用哪些成员函数(因为类模板的实例化定义并没有指出将会使用哪个成员函数)。因此,与处理类模板的普通实例化不同,遇到类模板的显式实例化时,编译器会实例化该类的所有成员。即使我们不使用某个成员,它也会被实例化。因此,我们用来显式实例化一个类模板的类型,它必须能用于类模板的所有成员,而不能像之前一样只适用于类模板中将被使用的(即将被实例化的)成员函数。 模板的隐式实例化
16.1.6 效率和灵活性???对模板设计者所面对的设计选择,标准库的智能指针类型给出了一个很好的展示。 ???如何处理删除器的差异实际上就是这两个类功能的差异。但是,如我们将要看到的,这一实现策略上的差异可能对性能有重要的影响。 在运行时绑定删除器(shared_ptr 删除器的工作方式)???虽然我们不知道标准库的具体实现(由编译器实现),但可以推断出,shared_ptr 必须能直接访问其删除器。即,删除器必须保存为一个指针或一个封装了指针的类(如标准库 function 类,参见 14.8.3 节)。 ???我们可以确定 shared_ptr 不是将删除器直接保存为一个成员,因为删除器的类型要到运行时才会知道。实际上,在一个 shared_ptr 的生存期中,我们可以随时改变其删除器的类型。我们可以使用一种类的删除器构造一个 shared_ptr,随后使用 reset 赋予此 shared_ptr 另一种类型的删除器。通常,类成员的类型在运行时是不能改变的。因此, shared_ptr 不能直接保存删除器。 ???为了考察删除器是如何正确工作的,我们假定 shared_ptr 将它所管理的指针保存在一个成员 p 中,且删除器是通过一个名为 del 的成员来访问的。则 shared_ptr 的析构函数必须包含类似以下语句:
???由于删除器是间接保存的,调用 在编译时绑定删除器(unique_ptr 删除器的工作方式)???现在,让我们来考察 unique_ptr 可能的工作方式。在这个类中,删除器类型是类类型的一部分。即,unique_ptr 有两个模板参数,一个表示它所管理的指针,另一个表示删除器类型。由于删除器的类型是 unique_ptr 的一部分,因此删除器成员的类型在编译时是知道的,从而删除器可以保存在 unique_ptr 对象中。 ???unique_ptr 的析构函数与 shared_ptr 的析构函数类似,也是对其保存的指针调用用户提供的删除器或执行 delete:
del 的类型或者是默认删除器类型,或者是用户提供的类型。到底是哪种情况没有关系,应该执行的代码在编译时肯定知道。实际上,如果删除器是类似于 DebugDelete (参见 16.1.4 节)之类的东西,调用可能会被编译为内联形式。 ???通过在编译时绑定删除器,unique_ptr 避免了间接调用删除器的运行时开销。通过在运行时绑定删除器,shared_ptr 使用户重载删除器更为方便。 16.2 模板实参推断(template argument deduction)???我们知道,对于函数模板,编译器利用函数调用中的函数实参来推断其模板参数(类型参数和非类型参数都是)。 16.2.1 类型转换与模板的类型参数???与非模板函数一样,我们在一次函数调用中传递给函数模板的实参被用来初始化函数形参。如果一个函数形参的类型使用了模板类型参数,那么采用特殊的初始化规则——只有很有限的几种类型转换会自动的应用于这些实参。编译器通常不是对实参类型进行转换,而是生成一个新的模板实例。 ???如果一个函数形参的类型使用了模板类型参数,顶层 const 无论在形参还是实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括以下两项:
????其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于使用了模板的类型参数或非类型参数的函数参数。 ????下面是一个例子:
???在 fobj 中,数组会自动转换为指针。但是在 fref 中,因为模板形参是引用,所以最后一个调用是错误的。 使用相同模板参数类型的函数形参????一个模板类型参数可以用作多个函数形参的类型。如果编译器根据实参推断出的类型(不是实参原来的类型)不匹配,则调用就是错误的。 ????如我们有:
???当我们的调用方式为:
???这是错误的,因为 lng 是 long 类型,是 1024 是 int 类型,而且无法使用算术类型的隐式转换。所以该调用无法实例化 compare 函数。 ????如果想要允许对函数实参的类型转换(不是指在用函数实参初始化函数形参时的转换,而是指形参被初始化后被使用时),我们可以为函数模板定义两个类型参数:
???这样,便可以提供不同类型的实参。如上述调用是合法的。当然了,必须定义了能比较这些类型的值的 < 运算符,因为这里用到了。 正常类型转换应用于普通函数实参???在函数模板的函数参数列表中,可以有非模板类型参数定义的参数,即不涉及模板类型参数的类型。这种函数参数不进行特殊处理;它们正常使用普通函数的参数转换规则。如下函数模板:
???我们可以看见,该函数模板的第一个参数是 ostream& ,所以当我们调用此函数时,传递给它的实参会进行正常的类型转换:
16.2.2 函数模板显式实参???在某些情况下,编译器无法推断出模板实参的类型。在其他一些情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。 指定显式模板实参???作为一个允许用户指定使用类型的例子,我们将定义一个名为 sum 的函数模板,它接受两个不同类型的参数。我们希望允许用户指定结果的类型。这样,用户就可以选择合适的精度。 ????我们可以定义表示返回类型的第三个模板参数,从而允许用户控制返回类型:
???编译器无法推断 T1,因为它未出现在函数参数列表中。所以每次调用 sum 时调用者必须为 T1 提供一个显式模板实参(explicit template argument)。 ????给函数模板提供显式模板实参的方式与定义类模板实例的方式相同。在函数名之后,实参列表之前用尖括号指明。
????显式模板实参按由左至右的顺序与对应的模板参数匹配,即第一个模板实参与第一个模板参数匹配,依此类推。只有后端 (右边) 参数的显式模板实参才可以忽略,前提是它们可以从函数参数推断出来或具有默认实参。 ????如我们的 sum 函数如下编写:
???我们总是必须提供三个形参指定实参:
正常类型转换应用于显式指定的实参???对于用普通类型定义的函数参数,允许进行正常的类型转换。出于同样的原因,对于模板类型参数已经显式指定了的函数参数,或是具有默认实参的函数参数,也将进行正常的类型转换:
16.2.3 尾置返回类型于类型转换???当我们希望用户确定返回类型时,用显式模板实参表示模板函数的返回类型时很有效的。但在其他情况下,要求显式指定模板实参会给用户增添额外负担。例如,我们可能希望编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用:
???我们 不知道返回结果的精确类型,但知道所需类型是所处理的序列的元素类型(即返回类型可从函数参数中间接或直接获取):
???我们知道,函数返回 *beg ,而我们能够通过 decltype(*beg) 来获取此表达式的类型。但是,在编译器遇到函数的参数列表之前,beg 都是不存在的。所以,为了定义此函数,我们必须使用尾置返回类型。由于尾置返回类型出现在参数之后,它可以使用函数的参数:
进行类型转换的标准库类模板???有时我们无法直接获得所需要的类型。例如,我们可能希望编写一个类似 fcn 的函数,它返回一个元素的值而非引用。 ????在编写这个函数的过程中,我们面临一个问题:对于传递的参数的类型,我们几乎一无所知。在此函数中,我们知道唯一可以使用的操作是迭代器操作,而所有迭代器操作都不会生成元素,只能生成元素的引用。 ????为了获得元素类型,我们可以使用标准库的类型转换模板。这些模板定义在头文件 type_traits 中。下表列出了这些模板,我们将在 16.5 节中看到它们的关键实现。
????在本例中,我们可以用标准库的类型转换的类模板 remove_references 来获得元素类型。 remove_references 有一个模板类型参数和名为 type 的 public 类型成员。我们用给一个引用类型实例化 remove_reference,type 将表示被引用的类型。如:remove_references<int&>,其 type 为 int 。
???组合使用 remove_reference、 尾置返回类型及 decltype 关键字,我们就可以在函数中返回元素值的拷贝:
???注意, type 是一个类的类型成员, 而该类依赖于模板的一个类型参数(即 ???每个类型转换模板的工作方式都与 remove_reference 类似——每个类型转换模板都有一个名为 type 的 public 成员,它表示一个类型。此类型与模板自身的模板类型参数相关,其关系如模板名所示。如果不可能(或者没必要)转换模板实参,则 type 成员就是模板实参类型本身。 16.2.4 函数指针和实参推断???当我们直接用一个函数模板名 初始化一个函数指针 或 为一个函数指针赋值 时,编译器使用函数指针的类型来推断模板实参(对于函数指针来说,其类型还包括形参类型以及返回类型)。 ????例如,假定我们有一个函数指针,它指向的函数返回 int,接受两个实参,每个参数都是指向 const int 的引用。我们可以使用该指针指向 compare 的一个实例:
???pf1 中参数的类型决定了 T 的模板实参的类型。在这里,T 的模板实参类型为 int。指针 pf1 指向 compare 的 int 版本实例。如果不能从函数指针的类型确定模板实参,则产生错误。典型例子如下:
???在这里, func 能接受 string 与 int 的 compare 实例,所以无法确定 func 的实参的唯一实例化版本,此调用失败。 ????我们可以通过指定显式模板实参来消除这个歧义:
Note:当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。 16.2.5 模板实参推断和引用????为了理解如何从函数调用进行类型推断,考虑下面例子:
其中函数参数 p 是一个模板类型参数 T 的引用,对于函数形参是一个指向模板类型参数的引用,需要注意两点:
从左值引用函数参数推断类型(1)????当一个函数参数是模板类型参数的一个普通(左值)引用时(即,形如 T& ),普通的引用绑定规则告诉我们,只能传递给它一个左值。实参可以是 const 类型,也可以不是。如果实参是 const 的,则 T 被推断为 const 类型。
????此外,我们也可以传给它一个右值引用,这将在(引用折叠)中详细讲解。 ?????如果函数参数类型是模板类型参数的一个 const 普通(左值)引用时(即,形如 const T& ),我们可以传递给它任何类型的实参——一个对象( const 或 非 const )、右值(字面值、临时对象等)。当函数参数本身是 const 引用时,T 的类型推断的结果不会是一个 const 类型。因为 const 已经是函数参数类型的一部分;因此,它不会也是模板类型参数的一部分:
从右值引用函数参数推断类型(1)????当一个函数参数是模板类型参数的一个右值引用时(即,形如 T&& ),我们可以传给它一个右值。若传递给它一个右值,类型推断过程类似左值引用函数参数的推断过程。推断出的 T 的类型是该右值实参的类型:
????此外,我们也可以传给它一个左值,这将在下一小节(引用折叠)中详细讲解。 引用折叠(又称引用坍缩,reference collapsing)和右值引用参数——从左(右)值引用函数参数推断类型(2)????假定 i 是 int 对象,我们可能认为 f3(i) 这样是不合法的,毕竟 i 是一个左值,通常我们不能把一个左值绑定到右值引用。但是 C++ 在正常绑定规则之外定义了两个例外规则,从而在特定情况下允许这种绑定。这两个例外规则是标准库函数 move 正确工作的基础。 ?????第一个例外绑定规则影响右值引用参数的推断如何进行。如上述情况,当我们调用 f3(i) 时,编译器推断 T 为 int& ,而非 int 。 即 ,当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型为实参的左值引用类型。 ???? T 被推断为 int&,看起来好像 f3 的函数参数应该是一个类型 int& 的右值引用。一般情况下,我们不能(直接)定义一个引用的引用。但是通过 类型别名 或 模板类型参数 间接定义是可以的。 ????在这种情况下,我们有 第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下,引用会折叠成一个普通的左值引用类型。在 C++11 中,有一个例外:右值引用的右值引用为折叠成右值引用。 即,对于一个给定类型 X :
编写接受右值引用参数的模板类型????模板参数可以推断为一个引用类型,这一特性对模板内的代码可能有令人惊讶的影响:
??当我们对一个右值调用 f3 的时候,例如字面常量 42,T 为 int。在此情况下,局部变量 t 的类型为 int,通过拷贝参数 val 的值被初始化。当我们对 t 赋值时,参数 val 保持不变。 ?????当代码中涉及的类型可能是普通(非引用)类型,也可能是引用类型时,编写正确的代码就变得异常困难(虽然 remove_reference 这样的类型转换类可能会有帮助)。 ?????在实际中,在函数模板中使用右值引用通常会用于两种情况:模板转发其实参或模板被重载。 ?????目前应该注意的是,使用右值引用的函数模板通常使用我们在 13.6.3 节中看到的方式进行重载:
与非模板函数一样,第一个版本将绑定到可修改(即非 const )的右值,此版本对于非 const 的右值是精确匹配(也是更好的匹配)。 16.2.6 理解 std::move????标准库函数 std::move 是使用右值引用的函数模板的一个很好的例子。 std::move 是如何定义的????标准库是这样定义 move 的:
static_cast 是关键字。 ????我们可以发现,函数形参是 T&&,所以我们可以传递给 move 左值(由于引用折叠)或者右值:
std::move 是如何工作的????在第一个赋值中,传递给 move 的实参是 string 的构造函数的右值结果——
????这个调用实例化 ?????考虑第二个赋值,传递给 move 的实参是一个左值:
????因此,这个调用实例化 move<string&>,即:string&& move(string &t),通过类型转换,得到 string&&。 从一个左值 static_cast 到一个右值引用是允许的????通常情况下, static_cast 关键字只能用于其他合法的类型转换。但是有一条针对右值的特许规则:虽然不能隐式地将一个左值转换为右值引用,但我们可以用 static_cast 显式地将一个左值转换为一个右值引用。但是要注意,右值引用并不是右值。
????最后, 虽然我们可以直接编写这种类型转换代码, 但使用标准库 move 函数是容易得多的方式。 而且, 统一使用 std::move 使得我们在程序中查找潜在的截断左值的代码变得很容易。 16.2.7 转发????某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是 const 的以及实参是左值还是右值。 ?????作为一个例子,我们将编写一个函数,它接受一个可调用表达式和两个额外实参。我们的函数将调用给定的可调用对象,将两个额外参数逆序传递给它。下面是我们的翻转函数初步模样:
????这个函数一般情况下没有问题。但当我们用它调用一个接受引用参数的函数时就会出现问题:
????在这段代码中 f 改变了绑定到 v2 的实参的值。 但是, 如果我们通过 flip1 调用 f , f 所做的改变就不会影响实参。
????问题在于 flip1 传递给 f 的参数 t1 。此参数类型是一个普通的、非引用的类型 int ,而非 int& 。因此,这个 flip1 调用被实例化为:
j 被拷贝到 flip1 函数参数 t1 中。 f 中的应用参数被绑定到函数形参 t1 ,而非原始实参 j 。 定义能保持类型信息的函数参数????为了通过翻转函数传递一个引用,我们需要重写函数,使其参数能保持给定实参的“左值性”。更进一步,我们也希望保持参数的 const 属性。 ????通过将一个函数参数定义为一个指向模板类型参数的右值引用(即 T&& t),我们可以保持其对应实参的所有类型信息。
??对于 flip2 ,当我们这样调用时 flip2(f, j, 42),j 的值将会发生改变,因为 j 是一个左值,我们将其绑定到右值引用时,T1 会被推断为 int&,引用折叠后 t1 也就是 int&,所以 t1 会被绑定到 j。
?????flip2 对接受左值引用的函数工作没有问题,但不能用于接受右值引用参数的函数,例如:
????如果我们试图通过 flip2 调用 g (无论传递给 flip2 的是左值还是右值),则参数 t2 将被传递给 g 的右值引用参数,即使我们传递一个右值给 flip2 :
????上述代码会出现:不能从一个左值实例化 int&& 的错误(!!注意:类型为右值引用的变量,它还是一个变量,而变量是左值)。函数参数和其它任何变量一样,都是左值表达式。 在调用中使用 std::forward 保持类型信息????我们可以使用一个名为 forward 的新标准库设施来传递 flip2 的参数,它能保持原始实参的类型。类似 move , forward 定义在头文件 utility 中。与 move 不同, forward 必须通过显式模板实参来调用。 forward 返回类型是该显式实参类型的右值引用。即,
本例中我们使用 Type 作为 forward 的显式模板实参类型,它是从 arg 推断出来的。由于 arg 是一个模板类型参数的右值引用, Type 将表示传递给 arg 的实参的所有类型信息。
?????使用 forward ,我们可以再次重写翻转函数:
????如果我们调用 flip(g, i, 42),i 将以 int& 类型传递给 g,42 将以 int&& 类型传递给 g。
16.3 重载与模板????函数模板可以被另一个模板函数或普通非模板函数重载。跟普通函数重载一样,名字相同的函数必须具有不同数量或类型的参数。 ????如果涉及到函数模板,则函数匹配规则会在下面几个方面受到影响:
前两点是互补的,候选函数:同名非模板的函数(不一定可行) + 同名的可行的函数模板实例(相比普通非模板函数已经经历了可行函数的检测,不然不会实例化) 编写重载模板????作为一个例子,我们将构造一组函数,它们在调试中可能很有用。我们将这些调试函数命名为 debug_rep ,每个函数都返回一个给定对象的 string 表示。我们首先编写此函数的最通用版本,将它定义为一个模板,接受一个 const 对象的引用:
此函数可以用来生成一个对象对应的 string 表示,该对象可以是任意具有输出运算符的类型。
注意此函数不能用于打印字符指针。因为IO库为 ????我们可以这样使用这些函数:
对于这个调用,只有第一个版本的 debug_rep 是可行的。第二个 debug_rep 版本要求一个指针参数,但在此调用中我们传递的是一个非指针对象。因此编译器无法从一个非指针实参实例化一个期望指针类型参数的函数模板,因此实参推断失败。由于只有一个可行函数,所以此函数被调用。 ????如果我们用一个指针调用 debug_rep :
两个函数都能生成可行的实例:
第二个版本的 debug_rep 实例是此调用的精确匹配。第一个版本的实例需要进行普通指针(string)到 const 指针(const string*)的转换。正常函数匹配规则告诉我们应当选择第二个模板,实际上编译器确实选择了这个版本。 多个可行的函数模板实例????作为另外一个例子,考虑以下调用:
??此例中的两个函数模板实例都是可行的,且都是精确匹配:
此时,正常函数匹配规则无法区分这两个实例,我们可能会觉得这个调用有二义性。但,根据重载函数模板的特殊规则,此调用被解析为 非模板和模板的重载????作为下一个例子,我们将定义一个普通非模板版本的 debug_rep 函数来打印双引号包围的 string :
此时,同样有2个可行函数:
此时,虽然两个函数具有相同参数列表,显然两者提供同样好的匹配。但是,编译器会选择非模板版本。因为有多个同样好的函数模板实例时,编译器会选择最特例化的版本,处于相同的原因,一个非模板函数比一个函数模板更特例化。
重载模板和类型转换????有一种情况还没讨论到:C风格字符串指针和字符串字面常量。现在还有一个接受 string 的 debug_rep 版本,我们可能期望一个传递字符串的调用会匹配这个版本。但是考虑下面这个调用:
本例中,所有三个 debug_rep 版本都是可行的:
对给定实参来说,两个模板都提供精确匹配——第二个模板要进行一次(许可的)数组类型到指针类型的转换,而对于函数匹配来说,这种转换被认为是精确匹配(参见 6.6.1 节)。非模板版本是可行的,但需要进行一次用户定义的类型转换,因此它没有精确匹配的优先极高,所以有两个模板成为可能调用的。和之前一样,编译器会选择第二个版本实例,因为 T* 版本更加特例化。 ????如果希望将字符串按 string 处理,可以定义另外两个非模板重载版本:
缺少声明可能导致程序行为异常????值得注意的是,为了使 char* 版本的 debug_rep 正确工作,在定义此版本时, debug_rep(const string&) 的声明必须在作用域中;否则,可能调用错误的 debug_rep 版本:
????通常,如果使用了一个忘记声明的函数,代码将编译失败。但对于具有重载函数模板的函数实例而言,则不是这样。如果编译器可以从模板实例化出与调用匹配的版本,则缺少的声明就不重要了。当然这也会导致程序行为异常,且难发现。在本例中,如果忘记声明非模板的接受一个 string 的 debug_rep 版本,编译器就会默默地实例化接受 const string& 的模板版本。 16.4 可变参数模板(variadic template)????一个可变参数模板(variadic template)就是接受一个可变数目参数的模板(函数或类)。可变数目的参数被称为参数包(parameter packer)。存在两种参数包:模板参数包(template parameter packer),表示零个或多个模板参数;函数参数包(function parameter packer),表示零个或多个函数参数。 ???我们用一个省略号(
??声明了 foo 是一个可变参数函数模板,它有一个名为 T 的类型参数,和一个名为 Args 的模板参数包。这个包表示零个或多个额外的类型参数。foo 的函数参数列表包含一个 const & 类型的参数,指向 T 的类型,还包含一个名为 rest 的函数参数包,此包表示零个或多个函数参数。 ????与往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断(模板/函数)包中的参数数目。例如,给定下面调用:
?编译器会为 foo 实例化出四个不同的版本:
??在每个实例中,T 的类型都是从第一个实参的类型推断出来的。剩下的实参(如果有)提供函数额外实参的数目和类型。 sizeof… 运算符????当我们需要知道包中有多少元素时,可以用
16.4.1 编写可变参数函数模板????在6.2.6节中我们知道,可以用 initializer_list 来定义一个可接受可变数目实参的函数。但是,所有实参必须具有相同的类型(或它们的类型都可以转换为某一个类型)。 ?????所以,当我们既不知道想要处理的实参的数目也不知道它们的类型时,可变参数函数(使用了模板参数包和函数参数包的函数)是很有用的。作为一个例子,我们将定义一个名为 print 的函数,它在一个给定流上打印给定实参列表的内容。 ????可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。我们的 print 函数也是如此。为了终止递归,我们还需要定义一个非可变参数的 print 函数,它接受一个流和一个对象:
第一个版本的 print 负责终止递归并打印初始调用的最后一个实参,第二个版本的 print 是可变参数版本,它打印绑定到 t 的实参,并调用自身来打印函数参数包中的剩余值。 ?????这段程序的关键部分是可变参数函数中对自身的递归调用:
????我们可以发现,可变参数版本的 print 有三个参数,os,const T& 与 一个参数包。而此调用只传递了两个实参。其结果是 rest 中的第一个实参被绑定到 t,剩余实参形成下一个 print 调用的参数包。当此包中只剩下一个参数时,虽然两个版本的 print 都能够精确匹配,但是非函数模板优先于函数模板,所以最后一个 print 调用的非函数模板的 print。
16.4.2 包扩展(packer expand)????对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展(expand)它。当扩展一个包时,我们还要提供用于每个扩展元素的模式(pattern)。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(…) 来触发扩展操作。 ?????例如,我们的 print 函数包含两个扩展:
第一个扩展操作扩展模板参数包,为 print 生成函数参数列表。第二个扩展操作出现在对 print 的调用中。此模式为 print 调用生成实参列表。
最后两个实参的类型和模式一起确定了右端参数的类型。此调用被实例化为:
????第二个扩展发生在对 print 的(递归)调用中。在此情况下,模式是函数参数包的名字(即 rest)。此模式扩展出一个由包中元素组成的、逗号分隔的列表。因此,这个调用等价于:
理解包扩展——即debug_rep(rest)…与debug_rep(rest…)的区别????print 中的函数参数包扩展仅仅将包扩展为其构成元素(即 rest…),C++还允许更为复杂的扩展模式。例如,我们可以编写第二个可变参数函数,对其每个实参调用 debug_rep,然后调用 print 打印结果 string:
?????这个 print 调用
最后一条 print 调用??就好像我们这样编写代码一样:
??与之相对,下面的模式会编译失败:
??这段代码的问题是我们在 debug_rep 调用中扩展了 rest,它等价于:
??在这个扩展中,我们试图用一个具有五个实参的列表来调用 debug_rep,但并不存在与此调用匹配的 debug_rep 版本。debug_rep 函数不是可变参数,去没有哪个 debug_rep 版本接受五个参数。
16.4.3 转发参数包????在有了C++11后,我们可以组合使用可变参数模板与 forward 机制来编写函数,实现将其可变实参(所有类型信息)不变地传递给其他函数。作为例子,我们将为 StrVec 类添加一个 emplace_back 成员。标准库容器的 emplace_back 成员是一个可变参数成员模板,它用其实参管理的内存空间中直接构造一个元素。
??模板参数包扩展中的模式是 &&,意味着每个函数参数将是一个指向其对应实参的右值引用。
??emplace_back 的函数体调用了 chk_n_alloc (参见13.5节)来确保有足够空间容纳一个新元素,然后调用了 construct 在 first_free 指向的位置中创建一个元素。construct 调用中的扩展为:
??它既扩展了模板参数包 Args,也扩展了函数参数包 args。此模式生成如下形式的元素:
??其中 Ti 表示模板参数包中第 i 个元素的类型,ti 表示函数参数包中第 i 个元素。例如,假定 svec 是一个 StrVec,如果我们调用:
??construct 调用中的模式会扩展出:
?????通过在此调用中使用 forward,我们保证如果用一个右值引用调用 emplace_back,则 construct 也会得到一个右值 (这是 forward 可以保证的)。 16.5 模板特例化????编写单一模板,使之对任何可能的模板实参都是最合适的,都能实例化,这并不总是能办到。在某些情况下,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或做得不正确。其他时候,我们也可以利用某些特定知识来编写更高效的代码,而不是从通用模板实例化。当我们不能 (或不希望) 使用模板版本时,可以定义类或函数模板的一个特例化版本。
但是,只有当我们传递给 compare 一个字符串字面常量或一个字符数组时,编译器才会调用接受两个非类型模板参数的第二个版本的 compare。如果我们传递给它字符指针,就会调用第一个版本:
我们无法将一个指针转换为一个数组的引用,因此参数是 p1 和 p2 时,会调用第一个版本的 compare,第二个版本的 compare 是不可行函数。 ?????为了处理字符指针 (而不是数组),可以为第一个版本的 compare 定义一个 模板特例化(template specialization) 版本。一个特例化版本就是模板的一个独立定义,在其中一个或多个模板参数被指定为特定的类型或值。 关于显式(全)特例化要注意的是:
定义函数模板特例化——只能使用显式(全)特例化????当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应该使用关键字 template 后跟一个空尖括号对(<>)。空尖括号指出我们将为原模板的所有模板参数提供实参:
????需要注意的是:当我们定义一个特例化版本时,函数参数类型必须与一个先前声明的非特例化的模板中对应的类型匹配。本例中特例化的原模板如下:
????所以,T 为 const char*,我们的函数要求一个指向此类型 const 版本的引用。一个指针类型的 const 版本是一个常量指针而不是指向 const 类型的指针,所以在特例化版本中,函数参数的类型为 const char* const &。 函数重载与模板特例化????当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即我们为原模板的一个特殊实例提供了定义。要清楚一点:一个特例化版本本质上是一个实例,而非函数名的一个重载版本。
????我们选择将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数,这将会影响到函数匹配。 ????例如,我们已经定义了两个版本的 compare 函数模板,一个接受数组引用参数,另一个接受 const T&。我们还定义了一个特例化版本来处理字符指针,这对函数匹配没有影响。当我们对字符串的字面值常量调用 compare 时:
对此调用,两个函数模板都是可行的,且提供同样好的(即精确)匹配。但是,接受字符数组参数的版本更特例化,因此编译器会选择它。 关键概念:普通作用域规则应用于模板特例化为了特例化一个模板:
????对于普通类和函数,丢失声明的情况(通常)很容易被发现——因为这会导致编译器不能继续处理代码。但是,如果丢失了一个特例化版本的声明,编译器通常会实例化原模板,很容易产生模板及其特例化版本声明顺序导致的错误,而这种错误又很难查找。 Best Practices:模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。 类模板特例化????类模板也是可以显式(全)特例化的。 ????作为一个例子,我们将为标准库 hash 模板定义一个特例化版本,可以用它将用户自定义对象保存在无序容器中。因为默认情况下,无序容器使用
????在定义此特例化版本的 hash 时,唯一复杂的地方是:必须在原模板定义所在的命名空间中特例化它。我们可以向命名空间添加成员,首先打开命名空间:
?花括号对之间的任何定义都将成为命名空间 std 的一部分。
类模板的显式(全)特例化定义以 template<> 开始,<>指出正在定义一个全特例化的模板。在本例中,我们正在特例化的模板名为 hash,而特例化版本为 ????类似其他任何类,我们可以在类内或类外定义特例化版本的成员,在本例中就是在类外定义的。 ????默认情况下,为了处理特定关键字类型,无序容器会使用 key_type 对于的特例化 hash 版本和 key_type 上的 operator== 运算符。 ?????由于
类模板部分特例化????与函数模板不同,类模板的特例化不必为所有模板参数都提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性(比如是否是 const 或左值和右值)。一个类模板的部分实例化(partial specialization)本身是一个模板。在使用它时,用户必须为那些在部分特例化版本中未指定的模板参数提供实参。 ?????标准库 remove_reference 就是通过一系列的特例化版本来完成其功能的:
?????由于一个部分特例化版本本质是一个模板,所以我们首先定义模板参数。同样,部分特例化版本的名字与原模板的名字相同。对每个未完成确定 类型/值 的模板参数,在特例化版本的模板形参列表中都必须有一项与它对应。在类名之后,我们要为特例化的模板参数指定实参,这些实参列于模板名之后的尖括号对内。这些实参与原始模板中的模板参数按位置对应。
?????即,部分特例化的模板参数列表是原始模板的参数列表的一个子集或者是一个特例化版本。还指将模板参数中的某个指定为具体的类型:
只特例化成员而不是整个类模板????我们可以只特例化某一类模板的(无论是函数模板还是非模板)成员函数而不是特例化整个类模板。例如,如果 Foo 是一个模板类,包含一个成员 Bar,我们可以只特例化该成员:
?????所以,当我们用 int 之外的任何类型使用 Foo 时,其成员像往常一样进行实例化。当我们用 int 使用 Foo 时,Bar 之外的成员向往常一样进行实例化。如果我们使用 Foo 的 Bar 成员,则会使用我们的特例化版本。 |
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 2:47:48- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |