函数模板
template <typename T>
int compare(const T &v1, const T&v2);
-
尖括号<>中为模板参数列表,函数可使用模板参数类型。
①当调用函数模板时,编绎器用函数实参推断模板实参,使用模板实参创建模板的实例,实例化模板。
②模板参数可用来指定返回类型或函数形参类型,还可用于函数体内变量声明或类型转换。 -
类型参数前必须使用关键字class或typename。
-
非类型参数,例unsigned N。
①可在模板中定义非类型参数,非类型参数表示一个值而非一个类型。
②模板实例化时,非类型参数由用户提供或编绎器推断的值替代,这些值必须是常量表达式。
③非类型参数是整型时,实参必须是常量表达式;非类型参数是指针或(左值)引用时,实参必须具有静态的生存期,不可绑定到普通局部变量或动态对象。
④在需要常量表达式的地方,可使用非类型参数,例指定数组大小;指针参数也可用nullptr或值为0的常量表达式实例化。 -
函数模板可声明为inline或constexpr,将声明符放在模板参数之后,返回类型之前。
-
模板程序应尽量减少对实参类型的要求。
①例:参数定义为const的引用时,可保证函数能用于不能拷贝的类型,标准库(unique_ptr和IO类型)不可拷贝,内置类型及其他标准库类型允许拷贝。
②使用比较时,实参类型不必支持多种运算符,可选择一种运算符比较,或使用标准库关系运算符,例:less<Type>。 -
定义模板不生成代码,实例化模板时,编绎器生成代码。
①模板的头文件既包括声明也包括定义。
②编绎模板时,检查语法错误。
③模板使用时,编绎器检查实参数目及参数类型匹配。
④模板实例化时,发现类型相关错误,实参能否支持模板所要求的操作等。
模板实参与类型转换
-
编绎器根据函数实参推断模板参数,用模板实参生成的函数版本与给定的函数调用最为匹配。
①编绎器通常不对实参进行类型转换,直接生成新的模板实例。
②顶层const在形参和实参中都会被忽略。 -
函数模板参数自动应用类型转换的只有const转换及数组名或函数名到指针的转换:
①const转换,可将非const对象的引用或指针实参传递给const对象的引用或指针形参。
②数组和函数指针转换,形参是引用类型时,数组名不会转换为指针;形参不是引用类型时,数组名实参可转换为指向首元素的指针,函数名实参可转换为函数类型的指针。
③其他类型转换,如算术转换、派生类向基类的转换、用户定义的转换不能应用于函数模板参数。 -
普通函数实参可正常类型转换,函数参数类型不是模板参数时,可对实参进行正常的类型转换。
-
函数多个形参类型为相同模板参数时,实参有限的类型转换后必须具有相同的类型,若同一个模板类型,推断出的实参类型不一样,则调用错误。
函数模板显式实参
-
编绎器无法推断模板实参类型或希望用户控制模板实例化时,须显式指定模板实参。
①返回类型为模板参数且与形参列表类型不相同,函数实参类型无法推断返回类型,此时须为返回类型的模板参数指定显式模板实参。
②指定显式模板实参与定义类模板实例方式相同,在函数名之后实参列表之前的尖括号中显式给出模板实参,函数形参类型由实参类型推断。
③显式模板实参按从左到右顺序与对应的模板参数匹配,最右可由函数实参推断的模板参数可忽略。
④显式模板参数类型的函数实参,可进行正常的类型转换。 -
尾置返回类型
①函数模板接受一对迭代器并返回序列中元素的引用,不清楚元素的准确类型时,可用关键字decltype作用于解引用迭代器来获取元素的类型,但编绎器在函数列表之前无法获取元素,此时使用尾置返回类型,由于尾置返回出现在参数列表之后,可使用函数的参数。
②template <typename It>
auto fcn (It beg, It end) -> decltype(*beg) {}; -
参数类型未知,迭代器的所有操作不会生成元素,只能生成元素的引用,decltype(*beg)返回元素的引用,若要返回元素的值,可以使用标准库的类型转换模板,定义在头文件type_traits中。
①template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type{};
②remove_reference有一个模板参数T和一个public类型的type成员,解引用迭代器实例化模板参数,T推断为引用类型,则type成员表引用对象的类型。
③type是类的成员,依赖于模板参数,在返回类型声明中使用typename告知编绎器,type表示一个类型。
标准类型转换模板:
Mod<T>, 其中Mod为 若T为 则Mod<T>::type为
remove_reference X&或X&& X
否则 T
add_const X& 、const X或函数 T
否则 const T
add_lvalue_reference X& T
X&& X&
否则 T&
add_rvalue_reference X&或X&& T
否则 T&&
remove_pointer X* X
否则 T
add_pointer X&或X&& X*
否则 T*
make_signed unsigned X X
否则 T
make_unsigned 带符号类型 unsigned X
否则 T
remove_extent X[n] X
否则 T
remove_all_extents X[n1][n2]… X
否则 T
实参推断与引用
-
从函数指针实参推断类型
①函数指针初始化或赋值函数模板时,编绎器使用指针的类型推断模板实参,须满足每个模板参数,唯一确定其类型或值。 -
从左值引用参数推断类型
①当函数参数是模板参数的左值引用时(形如T&),绑定规则,只能传递左值(如变量或返回引用类型的表达式),函数实参可以是const类型,也可以不是;函数实参是const时,模板参数T推断为const。
②当函数参数是模板参数const左值引用时(形如const T&),绑定规则,可传递任何类型的实参,对象(const或非const)、临时对象或是字面常量值;函数实参是const时,T的类型推断不会是const,const是函数参数类型的一部分,不会是模板参数类型的一部分。 -
从右值引用参数推断类型
①当函数参数是模板参数的右值引用时(形如T&&),绑定规则,可传递右值,类型推断过程类似左值引用函数参数的推断过程。
②推断T的类型是右值实参的类型。
③接受右值引用参数的函数模板,需重载区分const及非const类型,(T&&)绑定非const右值,(const T&)绑定左值和const右值。
引用折叠
-
当函数参数是模板参数的右值引用时,不可使用左值调用函数模板,但C++语言在正常的绑定规则之外定义了两个例外规则,这两个例外规则是move标准库正确工作的基础。
①当函数参数是模板参数的右值引用时(如T&&),将左值传递给函数,编绎器推断模板参数为函数实参的左值引用类型;T被推断为函数实参左值引用类型,即函数参数是左值引用的右值引用,通常不能直接定义引用的引用,但可通过类型别名或模板参数间接定义。
②此时,可使用第二个例外绑定规则:间接创建的引用的引用,会形成折叠现象,引用会折叠成一个普通的左值引用类型。 -
新标准下折叠规则可扩展到右值引用(右值引用的右值引用)。
①折叠成左值引用:X& &、X& &&、X&& &折叠成X&。
②折叠成右值引用:X&& &&折叠成X&&。
③引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。 -
将右值引用类型推断规则和引用折叠规则组合在一起,传递左值给右值引用的函数参数,编绎器推断T为左值引用类型。
理解std::move
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
-
std::move的定义,如上。
①不能直接将右值引用绑定到左值上,但可用move函数获得绑定到左值的右值引用。
②move的函数参数T&&是模板参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配,既可以传递左值,也可以传递右值。 -
std::move是如何工作的
①当传递给move的实参是右值时,推断出模板类型为T,remove_reference值实例化,remove_reference<T>的type成员是T,move的返回类型是T&&,move的函数参数t的类型为T&&,调用实例化T&& move(T && t),调用结果就是实参本身右值引用。
②当传递给move的实参是左值时,推断出模板类型为T&,remove_reference引用实例化,remove_reference<T>的type成员是T,move的返回类型是T&&,move的函数参数t的类型为T& &&,折叠为T&,调用实例化T&& move(T &t),调用结果将右值引用绑定到左值。 -
static_cast可显式地将左值转换为右值引用。
①C++允许将右值引用绑定到左值截断左值,但可通过强制使用static_cast进行转换而阻止这种截断左值的转换。
②也可直接编写类型转换代码,但统一使用std::move使得在程序中查找潜在的截断左值的代码变得容易。
转发
-
函数将实参数量及类型不变地转发给其他函数
①需要保持实参的所有性质,实参类型const属性以及左右值属性。
②比如函数参数为可调用对象时,需函数保持可调用对象的左、右值属性及const属性。 -
定义能保持类型信息的函数参数,可将函数参数定义为模板参数的右值引用,可以保持实参的所有类型信息。
①使用引用参数(左值或右值)可以保持const属性,在引用类型中const是底层的。
②将函数参数定义为模板参数的右值引用如T1&&和T2&&,通过引用折叠可保持实参的左值/右值属性。 -
在调用中使用std::forward保持参数类型信息,与move不同,forward必须通过显式模板实参来调用。
①通过显式模板实参调用,返回显式实参类型的右值引用,即forward<T>的返回类型是T&&。
②forward作用于模板参数右值引用的函数参数,若传递右值,返回类型为右值引用;若传递左值,
通过返回类型上的引用折叠,返回类型为左值引用;forward可以保持给定实参的左值/右值属性。 -
如下,arg是模板参数的右值引用,Type表示传递给arg实参的所有类型信息。
①实参arg是右值,则Type是普通非引用类型,forward<Type>返回Type&&。
②实参arg是左值,通过折叠引用,Type推断为左值引用类型,返回类型是左值引用类型的右值引用,对forward<Type>的返回类型进行引用折叠,将返回一个左值引用类型。
③即函数参数是模板参数类型的右值引用(T&&),forward会保持实参类型的所有细节。
template<typename Type>
intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
}
类模板
template <typename T> class Blob {};//类模板
template <typename T>
ret_type Blob<T>::member-name(parm-list){};//类外部成员函数定义
-
编绎器不能为类模板推断模板类型参数,需在尖括号中显式提供模板实参列表。
①使用类模板时在模板名后加尖括号,内为模板实例类型,当类模板提供默认实参时,模板名后尖括号可为空。
②在类模板自己的作用域中,可直接使用模板名不提供实参,例Blob<T>与Blob。 -
实例化过的类模板,成员函数在使用时才实例化。
-
模板类静态数据成员
①相同类型实例化的模板类共享静态数据成员和静态成员函数。
②可通过类类型对象访问类模板的静态成员,也可使用作用域运算符访问静态成员,此时类名需包括模板实参。 -
模板类型别名
①可定义typedef来引用实例化后的类,不能直接引用模板。
②但新标准中可用using为类模板定义类型别名。
template <typename T> using twin = pair<T, T>;
twin<string> auth; //auth是一个pair<string, string>
成员模板
-
普通类和模板类都可以包含模板成员函数,称成员模板,成员模板不能是虚函数。
-
普通类的成员模板
①定义一个类,类中包含重载的函数调用运算符,接受一个指针并对指针执行delete,类似unique_ptr的删除器类型,删除器适用任何类型,所以将调用运算符定义为模板函数。
②成员函数模板的类型可由编绎器推断。
③unique_ptr析构函数会调用删除器的调用运算符,unique_ptr析构函数实例化时,调用运算符也会实例化。 -
类模板的成员模板
①类和成员各有各的独立模板。
②在类外定义成员模板时,须同时提供类模板和成员模板的模板参数,类模板参数在前,后跟成员模板参数。
③template <typename T>
template <typename U> ClassName <T> :: Function(U a, U b) {}; -
实例化成员模板
①实例化类模板的成员模板时,须同时提供类模板和函数模板的实参。
②通过对象调用成员模板,类模板参数显式提供,成员模板根据函数实参推断函数模板实参。
友元
-
类模板和友元
①如果类模板包含非模板友元,友元可访问所有模板实例。
②若友元自身是模板,类可以授权给所有友元模板实例,也可只授权给特定实例。 -
一对一友好关系,常见形式是建立相同实例友元间的友好关系,类模板与其他类或函数模板间。
-
通用和特定的模板友好关系,友元声明中使用与类模板不同的模板参数,可使所有实例为友元。
-
可令模板的类型参数为友元,将实例化模板的类型声明为友元。
template <typename T > class BlobPtr;
template <typename T > class Blob;
template <typename T>
bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob{
friend class BlobPtr<T>;
friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
};//友元可访问相同类型实例化的模板类成员,其他实例无访问权限。
//友元关系限定在相同类型实例化的Blob与BlobPtr及Blob相等运算符之间
template <typename T> class Pal;
class C{ //非模板类
friend class Pal<C>; //类C实例化的Pal是C的友元
template <typename T> friend class Pal2; //Pal2所有实例都是C的友元,该声明方式无须前置声明
};
template <typename T> class C2{ //C2是模板类
friend class Pal<T>; //相同实例化友元Pal
template <typename X> friend class Pal2; //Pal2所有实例是C2所有实例的友元,该声明方式无须前置声明
friend class Pal3; //非模板类,是C2所有实例的友元,无须前置声明
};
template <typename Type> class Bar{
friend Type; //实例化Bar的类型参数为友元
};
模板参数
-
参数与作用域
①模板参数遵循普通的作用域规则,模板参数名的可见范围在声明之后,至模板声明或定义结束之前。
②模板参数隐藏外层作用域中声明的相同名字。
③特定文件需要的所有模板声明通常一起放置在文件开始位置,出现于使用模板的代码之前。 -
默认模板实参
①可为类模板和函数模板提供默认实参。
②函数模板可为模板参数提供模板实参,也可为函数参数提供默认实参。
③使用默认实参的类模板时,模板名后尖括号可为空。 -
使用类的类型成员
①使用作用域运算符可访问static成员和类型成员,编绎器掌握类的定义,可知道通过作用域运算符访问的名字是类型还是static成员,但对于模板代码存在困难。
②默认情况下C++语言假定通过作用域运算符访问的名字不是类型,如果希望使用模板类型的类型成员,必须显式告诉编绎器该名字是一个类型,使用关键字typename实现。
③typename T::value_type;
控制实例化
-
模板实际使用时进行实例化,可能导致相同实例出现在多个对象文件中。
①当多个独立编绎的源文件使用相同的模板,并提供相同的模板参数时,每个文件中都有该模板的实例。
②新标准中,可以通过显式实例化避免实例化相同模板的开销,在一个文件中实例化定义,使用关键字extern在多文件间实例化声明。
③编绎器遇到extern模板声明时,不会在本文件中生成实例化代码,extern实例化声明表明程序在其他位置有该实例化的非extern声明(定义)。
④编绎器在使用模板时自动实例化,extern声明必须出现在使用此实例化版本的代码之前,否则该文件中单独存在实例化的模板,一个给定的实例化版本,可以有多个extern声明,只能有一个定义。
⑤每个实例化的声明,程序的某个位置必须有显式的实例化定义。 -
实例化定义会实例化所有成员
①类模板的实例化定义会实例化该模板的所有成员,包括内联成员函数。
②显式实例化类模板的类型,必须能用于模板的所有成员函数。
效率与灵活性
-
标准库指针类型shared_ptr和unique_ptr管理指针策略,前者共享指针所有权,后者独占指针。
①允许用户重载默认删除器,重载shared_ptr删除器很容易,在创建或reset指针时传递一个可调用对象即可。
②unique_ptr删除器类型是unique_ptr模板类型的一部分,用户须在定义时以显式模板实参的形式指定删除器类型。 -
在运行时绑定删除器
①在shared_ptr生存期中,可以随时改变删除器类型,可使用一种类型删除器构造一个shared_ptr,后使用reset改变另一种类型的删除器。
②删除器的类型直到运行时才会知道,而类成员的类型运行时不能改变,因此shared_ptr不可将删除器保存为成员。
③删除器类型为一个指针或一个封装了指针的类(如function)。
④通过运行时绑定删除器,shared_ptr使用户重载删除器更方便。 -
在编绎时绑定删除器
①unique_ptr删除器是模板类类型的一部分,即有两个模板参数,一个表示管理的指针类型,一个表示删除器的类型,因此删除器类型在编绎时是知道的。
②unique_ptr删除器可以直接保存在unique_ptr对象中。
③通过在编绎时绑定删除器,unique_ptr避免间接调用删除器的运行时开销。
重载与模板
-
函数模板可以被另一个模板或普通非模板函数重载,重载函数名字相同,参数类型或数量不同。
-
函数匹配规则发生变化:
①候选函数包括所有模板实参推断成功的函数模板实例。
②候选的函数模板总是可行的,因为模板实参推断会排除不可行模板。
③可行函数按类型转换排序,可用于函数模板的类型转换非常有限。
④恰有一个函数提供比任何其他函数更好的匹配,选择此函数。 -
如果有多个函数提供同样好的匹配,则:
①如果同样好的函数中只有一个非模板函数,选择此函数。
②如果同样好的函数中没有非模板函数,而有多个函数模板,其中一个模板比其他模板更特例化,则选择此模板。
③否则,此调用有歧义,举例:一个模板适用于任何类型,包括指针类型,另一个模板只适用于指针类型,则指针调用时选择只适用于指针的特例化模板。
④正确定义一组重载的函数模板需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解。 -
在定义函数之前,声明所有重载的函数版本,如若未声明最佳匹配的函数版本,则编绎器实例化中一个版本。
可变函数模板
-
接受可变数目参数的模板函数或模板类。
①可变数目参数称为参数包,模板参数包或函数参数包,表示零个或多个模板参数或函数参数。
②在模板参数列表中,class…或typename…指出接下来的参数表示零个或多个类型的列表。
③一个类型名后跟省略号表示零个或多个给定类型的非类型参数的列表。
④在函数参数列表中,如果参数的类型是模板参数包,则此参数也是函数参数包。
⑤编绎器从函数实参推断模板参数类型,还会推断包中参数的数目。
⑥当需要知道包中有多少元素时,可使用sizeof…(),不会对实参求值,返回常量表达式。 -
编写可变参数函数模板
①使用initializer_list定义接受可变数目实参的函数,所有实参类型相同,或可转换为同一个公共类型。
②可变参数函数模板实参数目未知,类型未知。
③可变函数通常是递归的,第一步调用处理包中的第一个实参,剩余实参调用本身。
④为了终止递归,需要定义一个非可变参数函数,否则无限递归。 -
包扩展
①扩展一个包时,需要提供每个扩展元素的模式,扩展一个包即将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表,通过在模式右边放一个省略号…来触发扩展操作。
②扩展中的模式会独立地应用于包中的每个元素。 -
转发参数包
①在新标准下,可以组合使用可变参数模板与forward机制编写函数,实现将参数不变地传递给其他函数。
②保持类型信息是两阶段的过程,为了保持实参中的类型信息,将函数参数定义为模板类型参数的右值引用。
③其次,传递实参时使用forward保持实参的原始类型。
模板特例化
-
模板特例化就是模板一个独立的定义,其中一个或多个模板参数被指定为特定的类型。
-
定义函数模板特例化
①当特例化函数模板时,必须为原模板中的每个模板参数提供实参。
②使用关键字template后跟一个空尖括号<>,指出正在实例化模板,须为原模板的所有模板参数提供实参。
③当定义一个特例化版本时,函数参数类型必须与先前声明模板中对应的类型匹配。 -
函数重载与模板特例化
①特例化的本质是实例化一个模板,而非重载它,因此特例化不影响函数匹配。
②特例化一个模板,原模板的声明必须在作用域中,使用实例之前,特例化版本的声明也必须在作用域中。
③对应普通类和函数,丢失声明的情况很容易发现,编绎器不能继续处理代码,但是,丢失了一个特例化版本的声明,编绎器可以用原模板生成代码。
④丢失特例化版本声明编绎器会实例化原模板,容易产生模板及其特例化版本声明顺序导致的错误,而这种错误很难查找。
⑤模板及其特例化版本应该声明在同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。 -
类模板部分特例化
①类模板的特例化不必为所有模板参数提供实参,可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。
②一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。
③只能部分特例化类模板,而不能部分特例化函数模板。
④部分特例化版本的模板参数列表是原始模板的参数列表的一个子集或是一个特例化版本。 -
特例化成员而不是类,可以只特例化特定成员函数而不是特例化整个模板。