🥬类模板
2022-6-15
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property

 
类模板(class template)是合成类的蓝本。与函数模板不同的是编译器不能推断出类模板的模板参数的类型。相反,使用类模板时必须提供额外的信息,这些信息将放在类模板名后的尖括号中。这些额外信息是模板实参列表(template arguments list),用于替换模板参数列表(template parameters list)
 
比如Stack,如果定它是int,那么它就是存整型的栈:
如果想改成存double类型的栈呢?需要改变栈的数据类型,直接改typedef那里也可以:
这依然治标不治本,虽然看起来就像是支持泛型一样,它最大的问题是不能同时存储两个类型,你就算是改也没法解决:
那就需要不停地做出各种数据类型版本的栈:
函数可以使用模板,类也是可以的
类模板的定义格式
 
类模板不支持自动推出类型,它不像函数模板,不指定也可以根据传入的实参去推出对应的类型的函数以供调用。
类模板实例化在类模板名字后跟<>,然后将实例化的类型放在<>中:
类模板名字不是真正的类,而实例化的结果才是真正的类。Stack是类名,Stack<int>才是类型。
 
 

定义类模板

与函数模板一样,类模板以关键字 template 跟随模板参数列表。在类模板的定义中(包括成员定义),可以如使用常规类型或值一样使用模板参数,这些模板参数将在使用时被提供实参。
如在 Blob 类模板中,返回值类型是 T&,当实例化 Blob 时,T 将被替换为指定的模板实参类型。
 

实例化类模板

当使用类模板时,必须提供额外的信息,这个额外信息就是显式模板实参(explicit template arguments),它们将被绑定到模板的参数上。编译器使用这些模板实参来实例化从模板中实例化一个特定的类。
 
编译器为每一个不同的元素类型生成不同的类:
将实例化两个完全不同的类:以 string 为元素的Blob 和以double为元素的Blob。类模板的每个实例都构成完全独立的类。类型 Blob<string> 和其它的 Blob 类型之间没有任何关系,亦没有任何特殊的访问权限。
 
在模板作用域中引用模板类型
谨记类模板名字不是类型的名字。一个类模板被用于实例化类型,而实例化的类型总是包含模板的实参。在类模板中通常不使用真实存在的类型或值作为模板的实参,而是使用模板自己的参数作为模板实参。比如:Blob中的data被定义为 shared_ptr<vector<T>> data; 。当实例化 Blob<int> 时,data将被实例化为 shared_ptr<vector<int>>
 
 

类模板的成员函数

与常规函数一样,可以给类模板在类体内或类体外定义成员函数。同样,定义在类体内的成员函数是隐式内联的。类模板的成员函数本身是常规函数。然而,类模板的每个实例都有自己的成员版本。因而,每个类模板的成员函数具有与类模板一样的模板参数。因而,在类模板外定义的成员函数以关键字template跟随类模板参数列表开始。并且,在类外定义成员函数必须说明其属于哪个类。而且这个类名由于是从模板实例化而来的,需要包含模板实参。当定义成员函数时,模板的实参与模板的参数一样。因而整个形式如下:
 
 
与其它定义在类模板外的成员函数一样,构造函数也是先声明类模板的模板参数:
此处亦是使用类模板自己的类型参数作为vecotr的模板实参
 
实例化类模板成员函数
默认情况下,类模板的成员函数只有在程序使用模板函数时才会实例化。如果一个成员函数没有用到,那么就不会被实例化。成员只有在使用到时才会实例化的事实,使得我们可以实例化一个类,其使用到的类型实参只符合模板操作的部分要求。通常,一个类模板实例化类的成员只有在使用时才会被实例化。
 

简化在类代码中使用模板类名

在类模板自身的作用域中,可以使用模板名而不需要实参。当在类模板的作用域内,编译器认为使用模板自己的地方就像是指定了模板实参为模板自己的参数。
 
 

在类模板体外使用类模板名字

当在类模板体外定义成员时,必须记住直到看到类名时才处于类地作用域中:
由于返回类型出现在类作用域的外面,所有必须告知返回类型是BlobPtr以其类型参数为实参的实例。在函数体内部,我们处于类的作用域中,所以在定义ret时不再需要重复模板实参,当不再提供模板实参,编译器认为我们使用与成员实例一样的类型实参。
 

类型模板和友元

当类定义中包含友元声明时,类和友元可以相互不影响的时模板或者不是模板。类模板可以有非模板的友元,授权友元访问其所有的模板实例。如果友元自身是模板,授权友元的类控制访问权限是授给模板的所有实例还是给特定的实例。

一对一友元

最常见的友元形式就是一个类模板与另一个模板(类或函数)的对应实例之间建立友元关系。如:Blob类模板和BlobPtr类模板之间的友元关系,以及相等性判断(==)操作符之间的关系:
为了指定模板(类或函数)的特定实例,必须首先声明模板本身。模板的声明包括模板的模板参数列表。
友元声明使用 Blob 的模板参数作为它们的模板实参。因而,这种友元被严格限定在具有相同类型的模板实参的 BlobPtr 和相等操作符的实例之间:
BlobPtr<char> 的成员可以访问 ca 的非共有部分,但 ca 与 ia 之间没有任何特殊的访问权限。
 

通用和特例(Specific)的模板友元

一个类可以让另一个模板的所有实例都是其友元,或者将友元限定在某一个特定的实例:
为了让所有的实例都是友元,友元的声明中所使用的模板参数必须与类模板所使用的不一样,就像这里的 C2 中的 T 和 Pal2 中的 X 一样。
 

与模板本身的类型参数成为友元

在新标准中,可以使得模板的类型参数成为友元:
这里指明所有用于实例化 Bar 的任何类型都是 Bar 的友元。这里需要指出的是尽管一个友元通常是一个类或函数,但是 Bar 也可以用内置类型进行实例化。这种友元关系是允许的,这样才可以将 Bar 用内置类型进行实例化。
 

模板的类型别名

类模板的一个实例就是一个类类型,与任何别的类类型一样,可以定义一个typedef来作为实例化类的别名:
由于模板不是类型,所以不能定义 typedef 作为模板的别名。也就是说不能定义 typedef 来指向 Blob<T>
 
然而,在新标准下可以用using 声明来指定类模板的别名:
 
模板类型别名是一族类的别名。当定义模板类型别名时,可以固定一个或多个模板参数:
这里将partNo定义为一族 pair 类,其中第二个成员是unsignedpartNo的使用者只能指定pair的第一个成员,不能对第二个成员做出选择。
 
 

类模板的静态成员

与其它类一样,类模板可以声明静态成员(static members)
Foo 的每个类实例都有自己的静态成员实例。也就是说对于每个给定类型 X,都有一个 Foo<X>::ctr 和一个 Foo<X>::count 成员,而 Foo<X> 的所有对象都共享相同的 ctr 对象和 count 函数。
 
与任何别的 static 数据成员一样,每个类实例必须只有一个static 数据成员的定义。然而,每个类模板的实例有一个完全不一样的对象,因而,在定义静态数据成员时与在类外定义成员函数类似:
与类模板的任何成员类似,定义的开始部分是模板参数列表,后跟随成员的的类型,然后是成员名字。成员名字中包含成员的类名,而此时的类名是从一个模板中生成而来的,所以包含模板的实参。因而,当 Foo 为每个特定义的模板实参进行实例化时,一个独立的 ctr 将为此类类型进行实例化并初始化为 0。
 
与非模板类的静态成员类似,可以通过类的对象或者使用作用域操作符对静态成员进行直接访问。当然,为了通过类使用一个静态成员,必须是一个特定的类实例才行:
与任何别的成员函数类似,静态成员函数仅在被使用时实例化。
 
 
 

模板参数

与函数参数名字类似,模板参数名字没有本质的含义。通常将类型参数记作T,但可以使用任何名字:
 

模板参数和作用域

模板参数遵循常规的作用域规则。模板参数的名字可以在其声明之后使用,直到模板的声明或定义的尾部。与任何别的名字类似,模板参数隐藏任何外部作用域的相同名字的声明。与绝大多数上下文不一致的是,作为模板参数的名字不能在模板中复用。如:
常规的名称隐藏规则导致 A 的类型别名被类型参数 A 所隐藏。由于不能复用模板参数的名字,将 B 声明为变量名是一个错误。
由于模板参数名字不能被复用,模板参数名字只能在给定模板参数列表中出现一次:
 

模板声明

模板声明必须包含模板的参数列表:
与函数参数一样,模板参数的名字不需要在声明和定义之间完全一样,如:
以上三个用法都是表示同一个函数模板。当然,给定模板的所有声明和定义都必须具有相同数目和种类(类型或非类型)的参数。
被某个文件所需要的所有模板声明都应该放在文件的头部,它们最好放在一起,并且是在所有使用这些名字之前就声明。
 

使用类的类型成员

使用作用域操作符(::)可以访问静态成员和类型成员(type member),在常规代码中,编译器是知道一个名字是类型成员或者是静态成员。然而,当遇到模板时,编译器很可能就无法知道这个信息了,如,给定T模板类型参数,当编译器看到 T::mem 时,它将直到实例化时才能直到 mem 是类型成员还是静态成员。然而,有时必须得让编译器知道一个名字表示类型才能正确编译。例如以下表达式:
编译器必须知道 size_type 是类型,这是在定义一个名字 p 的变量,不然,就不会被处理为静态数据成员 size_type 与变量 p 相乘。
默认情形下,C++认为通过作用域操作符访问的名字不是类型。如果要使用一个模板类型参数的类型成员,必须显式告知编译器这个名字是类型。那就得用 typename 这个关键字了:
以上函数期待一个容器作为其实参,使用 typename 类指定其返回类型,并且在没有元素的情况下生成一个值初始化的元素用于返回。
当想要告知编译器一个名字表示类型时,必须使用关键字 typename 而不是 class
 

默认模板实参

与可以给函数参数提供默认实参一样,可以提供默认模板实参(default template arguments),在新标准下可以给函数和类模板提供默认实参。早期的语言版本只允许给类模板提供默认实参。如:
这里同时提供了模板模板实参和模板函数实参。默认模板实参使用 less 函数对象的 T类型实例,而默认函数参数告知 fF 类型的默认初始化对象。使用 compare 时可以提供自己的比较操作,但不是必须这么做。如:
compare 以三个实参进行调用时,第三个参数的必须是可调用对象,并且返回值可以转为bool,其参数类型必须与前两个参数的类型可以转换。与函数默认参数一样,模板参数的默认实参只有在其右侧的所有参数都具有默认实参时才是合法的。
 

模板默认实参和类模板

无论何时使用类模板,都必须在模板名之后跟随尖括号。尖括号表示类是从一个模板实例化而来的。特别是,如果一个类模板给所有模板参数都提供了默认实参,并且我们也希望使用这些默认值,还是必须得在模板名字后提供一个空的尖括号对。如:
 
 
 
 
 

成员模板

无论是常规(普通)的类还是类模板都可以有一个本身就是模板的成员函数,这种成员被称为成员模板(member templates),成员模板一定不能是虚函数。
 

常规类的成员模板

常规类中的成员模板与模板函数的写法完全一样:
成员模板与别的模板一样,都是从自己的模板参数列表开始的。每个DebugDelete对象有自己的ostream成员,并且有一个成员函数本身是模板。用法如下:
 
也可以被用于构建 unique_ptr 对象:
以上当 unique_ptr 的析构函数被调用时,DebugDelete 的调用操作符将会被调用。因而,无论何时 unique_ptr 的析构函数被实例化,DebugDelete 的调用操作符将被实例化。
 
类模板的成员模板
可以给类模板定义成员模板,在这种情况下,类和成员的模板参数是各自独立的:
此构造函数有其自己的模板类型参数 It 。不同于类模板的常规成员函数,成员模板是函数模板。当在类模板外部定义成员模板时,必须提供同时为类模板和函数模板提供模板参数列表。先提供类模板的参数列表,紧跟着成员模板自己的模板参数列表:
 
初始化和成员模板
为了实例化类模板的成员模板,必须同时给类和函数模板同时提供模板参数。与往常一样,类模板的实参必须显式提供,而成员模板的实参则从函数调用中推断出来。:
当定义 a1 时,显式告知编译器去实例化模板参数绑定到 intBlob 版本。而其构造函数自己的类型参数则从begin(ia)中推断出来,在此例是 int*。因此,a1的实例定义是:
 

控制实例化

模板只有在使用时才会生成实例意味着同一个实例可能会出现在多个 obj 文件中。当两个或多个分离编译的源文件使用同一个模板且模板实参是一样的,那么在每个文件中都由一个此模板的相同实例。
在大的系统中,在多个文件中过度实例化同一个模板造成的影响将是很大的。在新标准中,可以通过显式实例化(explicit instantiation)来避免这个消耗。显式实例化的形式如下:
显式实例化的声明或定义之前,必须要能够看到模板体的代码。其中declaration是将所有的模板参数替换为模板实参:
当编译器看到extern模板声明时,它将不会生成在当前文件中生成实例代码。extern声明意味着在程序的某个地方存在着一个非extern的实例,程序中可以有多个extern声明,但是只能有一个定义。
由于当使用模板时会自动实例化,所以extern声明必须出现在所有使用此实例的代码之前。
函数模板必须被显式实例化,而类模板不一定需要,编译器会隐式实例化类模板。这个特性可能是编译器自己的特性,所以不应依赖于此。最好是对于每个实例声明都对应一个显式地实例定义。
 
实例定义实例化所有成员
类模板的实例定义实例化模板的所有成员,包括内联成员函数。当编译器看到一个实例定义时,它无法直到到底哪个成员函数将会程序使用,因此,于常规的类模板实例化不同的是,编译器将实例化类的所有成员。即便不使用某个成员,其也必须实例化。结果就是,我们只能显式实例化所有成员都可以使用的模板实例。
 

效率和灵活性

智能指针类型提供了描述模板设计者的设计选择很好的材料。shared_ptr 可以在创建或 reset 指针时传递一个删除器(deleter)来轻松覆盖之前的。而 unique_ptr 的删除器却是类型的一部分,我们必须在定义 unique_ptr 就显式提供一个类型作为模板实参,因而,给 unique_ptr 定制删除器会更加复杂。
删除器是如何被处理的与类的功能似乎并无本质上的关系,但这种实现策略却对性能有重大的影响。
在运行时绑定删除器
shared_ptr 的删除器是间接存储的,意味着可能作为指针或者一个包含指针的类,这是由于其删除器直到运行时才能被知道是何种类型,而且在其生命周期中还可以不断改变。一般来说,一个类的成员类型不会在运行时改变,所以此删除器必须是间接存储的。调用方式如:
 
在编译期绑定删除器
由于删除器的类型是作为 unique_ptr 的类型参数指定的,意味着删除器的类型可以在编译期就知道,因而,此删除器可以被直接存储。调用方式如:
这个方式的好处在于不论代码执行哪个类型的删除器,其都是就地执行,意味着不需要做任何跳转,甚至对于简单的函数可以内联到调用处。这是编译期绑定的功劳。
通过在编译期绑定删除器,unique_ptr 避免了调用删除器的运行时消耗;通过在运行时绑定删除器,shared_ptr 带来了灵活性,使其更容易定制新的删除器;
 
  • C++
  • 函数模板模板实参推断
    目录