C++ Primer(六) 模板与泛型编程
面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况。OOP能处理类型在程序运行之前都未知的情况;而泛型编程中,在编译时就可以获知类型。
定义模板
函数模板
模板定义以关键字 template开始,后接模板参数列表(template parameter list),模板参数列表表是用尖括号<>括住的一个或多个模板形参的列表,用逗号分隔,不能为空。使用模板时,我们显式或隐式地指定模板实参,将其绑定到模板参数上。
1 | template<typename T> |
模板类型参数(type parameter):类型参数前必须使用关键字class或者typename,这两个关键字含义相同,可以互换使用。旧的程序只能使用class。
非类型模板参数(nontype parameter):表示一个值而非一个类型。实例化模板时,非类型参数被一个用户提供的或编译器推断出的值所替代,这些值必须是常量表达式。
一个非类型参数可以是一个整型,或是一个指向对象或函数类型的指针或(左值)引用。
- 绑定到非类型整型参数的实参必须是一个常量表达式。
- 绑定到指针或引用非类型参数的实参必须具有静态的生存期。
不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参,可以用nullptr或一个值为0的常量表达式来实例化。
1 | template <unsigned N, unsigned M> |
函数模板可以声明为inline或constexpr的,这些说明符放在模板参数列表的后面。
1 | template <typename T> inline T min(const T&, const T&); |
模板程序应该尽量减少对实参类型的要求。
函数模板和类模板成员函数的定义通常放在头文件中。
类模板
类模板用于生成类的蓝图。不同于函数模板,编译器不能推断模板参数类型。
实例化类模板:提供显式模板实参列表,来实例化出特定的类。类模板中所有的实例都形成一个独立的类。
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
1 | template<typename T> class Blob{ |
在一个类模板作用域内,可以直接使用模板名而不必指定模板实参。
1 | template<typename T> class BlobPtr{ |
类与友元各自是否是模板相互无关。
新标准允许模板将自己的类型参数成为友元。
1 | template <typename T> class Bar{ |
因为模板不是一个类型,因此无法定义一个typedef引用一个模板(例如,无法定义一个typedef引用Blob
1 | template<typename T> using twin = pair<T, T>; |
类模板的每个实例都有一个独有的static对象,static成员函数只有在使用时才会实例化。
1 | template<typename T> class Foo { |
模板参数
模板参数名的可用范围是在声明之后,至模板声明或定义结束前。
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置。
当希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class。
1 | template<typename T> |
可以为函数和类模板提供默认实参,无论何时使用类模板,都需要为模板名后加上尖括号,使用类模板实参时,需要加上空的尖括号。
1 | template<typename T, typename F = less<T>> |
成员模板
一个类(普通类或者类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板(member template),成员模板不能是虚函数。
1 | class DebugDelete{ |
控制实例化
模板被使用时才会进行实例化,意味着,相同的实例可能出现在多个对象文件中。当多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中都会有该模板的一个实例。
但是在多个文件中实例化相同模板可能带来非常严重的额外开销。新标准下可以通过显式实例化(explicit instantiation)来避免这种开销。
1 | extern template declaration; //实例化声明 |
extern实例化声明表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。对每个实例化声明,在程序中某个位置必须尤其显式的实例化定义。
在一个类模板的实例化定义中,所用类型必须能用于模板的所有成员函数。
模板实参推断
模板实参推断(template argument deduction):对函数模板,编译器利用调用中的函数实参来确定其模板参数的过程。
类型转换与模板类型参数
如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则,只有有限的几种类型转换会自动地应用于这些实参。编译器通常不对实参进行类型转换,而是生成一个新的模板实例。
- 顶层
const无论是在形参中还是实参中,都会被忽略。 const转换,可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。- 数组或函数指针转换,如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。(数组实参可以转换为一个指向其首元素的指针,函数实参可以转换为一个函数类型的指针)。
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换,其他类型转换,如算术转换,派生类向基类的转换以及用户定义的转换不能应用于函数模板。
如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
函数模板显式实参
某些情况下,编译器无法推断出模板实参的类型。需要为模板提供一个显式模板实参。
显式模板实参在尖括号中给出,位于函数名之后,实参列表之前。显式模板实参按由左到右的顺序与对应的模板参数匹配。
1 | template <typename T1, typename T2, typename T3> |
模板类型参数已经显式指定了函数实参,则正常类型转换也能用于显式指定的实参。
尾置返回类型与类型转换
某些情况下,并不清楚返回结果的准确类型,但是知道所需类型与参数相关。则可以使用尾置返回类型(允许我们在参数列表之后声明返回类型)来获取模板相关实参类型。
1 | template<typename It> |
1 | template<typename It> |
标准库的类型转换(type transformation)模板,定义在头文件type_traits中。
对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 |
函数指针和实参推断
使用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数能唯一确定其类型或值。
1 | template<typename T> int compare(const T&, const T&); |
模板实参推断和引用
当一个函数参数时模板参数类型的一个普通(左值)引用时(T&),只能传递给它一个左值(变量或返回引用类型的表达式)。实参可以是const类型,也可以不是。如果是const的,则T被推断为const类型。
当函数参数类型是const T&,可以传递给它任意类型的实参(const非const对象,临时对象或是一个字面值常量)。当函数参数本身是const时,T的类型推断不会是const类型。
当一个函数参数时一个右值引用(T&&)时,只能传递一个右值,T的类型为该右值的实参类型。
1 | template<typename T>void f(T&&); |
通常不能将一个右值引用绑定到一个左值上,但是有两个例外:
- 当将一个左值传递给函数的右值引用参数,且右值引用指向模板类型参数时(如
T&&),编译器会推断模板类型参数为实参的左值引用类型。 - 如果我们间接(类型别名或者模板参数)创造一个引用的引用,则这些引用形成了折叠。折叠引用只能应用在间接创造的引用的引用,如类型别名或模板参数。通常引用会折叠成一个普通(左值)引用类型。只有在右值引用的右值引用下会折叠为右值引用。
| 引用折叠 | |
|---|---|
X& &、X& &&和X&& & |
折叠成类型X& |
类型X&& && |
折叠成X&& |
上面两个例外规则导致两个重要结果:
- 如果一个函数参数是一个指向模板类型参数的右值引用(如
T&&),则它可以被绑定到一个左值上; - 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个左值引用参数(
T&)。
如果一个函数参数是指向模板参数类型的右值引用(T&&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&)。
理解std::move
1 | template <typename T> |
从一个左值static_cast到一个右值引用是允许的。
转发
将一个函数参数定义为指向模板类型参数的右值引用可以保持其对应实参的所有类型信息。例如对应实参的const属性和左值/右值属性将得到保持。
名为forward的新标准库设施来传递参数,它能够保持原始实参的类型。定义在头文件utility中。必须通过显式模板实参来调用。forward返回显式实参类型的右值引用。即,forward<T>的返回类型是T&&。
当用于一个指向模板参数类型的右值引用函数参数(T&&)时,forward会保持实参类型的所有细节。
1 | template<typename F, typename T1, typename T2> |
重载与模板
多个可行模板:当有多个重载模板对一个调用提供同样好的匹配时,会选择最特例化的版本。
非模板和模板重载:对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
可变参数模板
可变参数模板就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包(parameter packet)。模板参数包:标识另个或多个模板参数。函数参数包:标识另个或者多个函数参数。用一个省略号来指出一个模板参数或函数参数,表示一个包。
template <typename T, typename... Args>,Args第一个模板参数包。
void foo(const T &t, const Args& ... rest);,rest是一个函数参数包。
sizeof...运算符,返回参数的数目。
编写可变参数函数模板
可变参数函数通常是递归的:第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
1 | //该版本负责终止递归并打印最后一个实参。 |
包扩展
对于一个参数包,除了获取它的大小,唯一能做的事情就是扩展(expand)。扩展一个包时,还要提供用于每个扩展元素的模式(pattern)。扩展中的模式会独立地应用于包中的每个元素。
1 | template <typename... Args> |
转发参数包
新标准下可以组合使用可变参数模板和forward机制,实现将实参不变地传递给其他函数。
1 | class StrVec{ |
模板特例化(Specializations)
定义函数模板特例化:关键字template后面跟一个空尖括号对(<>)。
特例化的本质是实例化一个模板,而不是重载它。特例化不影响函数匹配。
模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是特例化版本。
我们可以部分特例化类模板,但不能部分特例化函数模板。
