一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。
模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用小于号(<)和大于号(>)包围起来。
在模板定义中,模板参数列表不能为空。
模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们(隐式地或显式地)指定模板参数,将其绑定到模板参数上。
当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。即,编译器使用实参的类型来确定绑定到模板参数T的类型。
编译器用推断出的模板参数来为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。这些编译器生成的版本通常被称为模板的实例。
我们可以将模板类型参数看作类型说明符
// 正确:返回类型和参数类型相同 template <typename T> T foo(T** p) { T tmp = *p; // tmp的类型将是指针p指向的类型 // ... return tmp; }
类型参数前必须使用关键字class或typename:
// 错误:U之前必须加上class或typename template <typename T, U> T calc(const T&, const U&);
在模板参数列表中,这两个关键字的含义相同,可以互换使用。一个模板参数列表可以同时使用这两个关键字。
一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字class或typename来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所替代。这些值必须是常量表达式,从而允许编译器在编译时实例化模板
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。指针参数也可以用nullptr或一个值为0的常量表达式来实例化。
在模板定义中,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。
非类型模板参数的模板实参必须是常量表达式。
函数模板可以声明为inline或constexpr的,如同非模板函数一样。inline或constexpr说明符放在模板参数列表之后,返回类型之前。
编写泛型代码的两个重要原则:
模板程序应该尽量减少对实参类型的要求。
当我们使用(而不是定义)模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。
通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
函数模板和类模板成员函数的(声明和)定义通常(都)放在头文件中。
当使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的提供者来保证的。而且,模板的提供者必须保证,当模板被实例化时,模板的定义,包括类模板的成员的定义,也必须是可见的。用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须是可见的,这是由模板的用户来保证的。
通常,编译器会在三个阶段报告错误:
- 第一个阶段是编译模板本身时 - 第二个阶段是编译器遇到模板使用时 - 第三个阶段是模板实例化时
当我们编写模板时,代码不能是针对特定类型的,但模板代码通常对其所使用的类型有一些假设(比如说默认定义了<运算符)。
保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。
类模板是用来生成类的蓝图的。编译器不能为类模板推断模板参数类型。为了使用类模板,我们必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
一个类模板的每个实例都形成一个独立的类。类型Blob<string>
与任何其他Blob
类型都没有关联,也不会对任何其他Blob
类型的成员有特殊访问权限。
为了阅读模板类代码,应该记住类模板的名字不是一个类型名。类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。
一个类模板中的代码如果使用了另外一个模板,通常不将一个实际类型(或值)的名字用作其模板实参。相反的,我们通常将模板自己的参数当作被使用模板的实参。
std::shared_ptr<std::vector<T>> data
我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。
类模板的每个实例都有其自己版本的成员函数。类模板的成员函数具有和模板相同的模板参数。定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。
template <typename T> ret-type Blob<T>::member-name(parm-list)
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。如果一个成员函数没有被使用,则它不会被实例化。这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。
当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参(在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参)。
// 在class中 BlobPtr& operator++(); // 前置运算符 BlobPtr& operator--(); // 和下等同 BlobPtr<T>& operator++(); BlobPtr<T>& operator--();
当我们在类模板外定义其成员时,必须记住,我们并不在类的作用域中,直到遇到类名才表示进入类的作用域。
类模板与另一个(类或函数)模板间友好关系的最常见的形式是建立对应实例及其友元间的友好关系。
为了引用(类或函数)模板的一个特定实例,我们必须首先声明模板自身。一个模板声明包括模板参数列表:
// 前置声明,在Blob中声明友元所需要的 template <typename> class BlobPtr; template <typename> class Blob; // 运算符==中的参数所需要的 template <typename T> bool operator==(const Blob<T>&, const Blob<T>&); template <typename T> class Blob { // 每个Blob实例将访问权限授予相同类型实例化的BlobPtr和相等运算符 friend class BlobPtr<T>; friend bool operator==<T> (const Blob<T>&, const Blob<T>&); // 其他成员定义 };
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元:
// 前置声明,在将模板的一个特定实例声明为友元时要用到 template <typename T> class Pal; class C { // C是一个普通的非模板类 friend class Pal<C>; // 用类C实例化的Pal是C的一个友元 // Pal2的所有实例都是C的友元;这种情况无需前置声明 template <typename T> friend class Pal2; }; template <typename T> class C2 { // C2本身是一个类模板 // C2的每个实例将相同实例化的Pal声明为友元 friend class Pal<T>; // Pal的模板声明必须在作用于之内 // Pal2的所有实例都是C2的每个实例的友元,不需要前置声明 template <typename X> friend class Pal2; // Pal3是一个非模板类,它是C2所有实例的友元 friend class Pal3; // 不需要Pal3的前置声明 };
为了让所有实例称为友元,友元声明中必须使用与类模板本身不同的模板参数。
我们可以将模板类型参数声明为友元:
template <typename Type> class Bar { friend Type; // 将访问权限授予用来实例化Bar的类型 // ... };
类模板的一个实例定义了一个类类型,与任何其他类类型一样,我们可以定义一个typedef来引用实例化的类:
typedef Blob<string> StrBlob;
新标准允许我们为类模板定义一个类型别名:
template<typename T> using twin = pair<T, T>; twin<string> authors; // authors是一个pair<string, string>
与任何其他类相同,类模板可以声明static成员
template <typename T> class Foo { public: static std::size_t count() { return ctr; } // 其他接口成员 private: static std::size_t ctr; // 其他实现成员 };
每个Foo
的实例都有其自己的static成员实例。所有Foo<X>
类型的对象共享相同的ctr
对象和count
函数。
与定义模板的成员函数类似,我们将static数据成员也定义为模板:
template <typename T> size_t Foo<T>::ctr = 0; // 定义并初始化ctr
为了通过类来直接访问static成员,我们必须引用一个特定的实例:
Foo<int> fi; // 实例化Foo<int>类和static数据成员ctr auto ct = Foo<int>::count(); // 实例化Foo<int>::count ct = fi.count(); // 使用Foo<int>::count ct = Foo::count(); // 错误:使用哪个模板实例的count?
模板参数遵循普通的作用域规则。一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是,与大多数其他上下文不同,在模板内不能重用模板参数名:
typedef double A; template <typename A, typename B> void f(A a, B b) { A tmp = a; // tmp的类型为模板参数A的类型,而非double double B; // 错误:重声明模板参数B }
由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次:
// 错误:非法重用模板参数名V template <typename V, typename V> //...
模板声明必须包含模板参数;与函数参数相同,声明中的模板参数的名字不必与定义中相同;当然,一个给定模板的每个声明和定义必须有相同数量和种类(即,类型或非类型)的参数。
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
默认情况下,C++语言假定通过作用域运算符访问的名字不是类型(而是静态成员)。
当我们希望通知编译器一个名字表示类型时,必须使用关键字typename
,而不能使用class
。
template <typename T> typename T::value_type top(const T& c) // value_type是一个类型而不是静态成员 { if (!c.empty()) return c.back(); else return typename T::value_type(); }
我们可以为函数和类模板提供默认实参。
// compare有一个默认模板实参less<T>和一个默认函数实参F() template <typename T, typename F = less<T>> int compare(const T &v1, const T &v2, F f = F()) { if (f(v1, v2)) return -1; if (f(v2, v1)) return 1; return 0; }
与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
无论如何使用一个类模板,我们都必须在模板名之后接上尖括号。特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号。
template <class T = int> class Numbers { // T默认为int public: Numbers(T v = 0): val(v) { } // 对数值的各种操作 private: T val; }; Numbers<long double> lots_of_precision; Numbers<> average_precision; // 空<>表示我们希望使用默认类型
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板。成员模板不能是虚函数。
我们可以通过显式实例化来避免在多个文件中实例化相同模板带来的额外开销。
extern template declaration; // 实例化声明 template declaration; // 实例化定义
declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参。
当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前:
// Application.cc // 这些模板类型必须在程序其他位置进行实例化 extern template class Blob<string>; extern template int compare(const int&, const int&); Blob<string> sa1, sa2; // 实例化会出现在其他位置 // Blob<int>及其接受initializer_list的构造函数在本文件中实例化 Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9}; Blob<int> a2(a1); // 拷贝构造函数在本文件中实例化 int i = compare(a1[0], a2[0]); // 实例化出现在其他位置
// templateBuild.cc // 实例化文件必须为每个在其他文件中声明为extern的类型和函数提供一个(非extern)的定义 template int compare(const int&, const int&); template class Blob<string>; // 实例化类模板的所有成员
当编译器遇到一个实例化定义(与声明相对)时,它为其生成代码。
对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。
在一个类模板的实例化定义中,所用类型必须能用于模板的所有成员函数。
通过在编译时绑定删除器,unique_ptr
避免了间接调用删除器的运行时开销。通过在运行时绑定删除器,shared_ptr
使用户重载删除器更为方便。
从函数实参来确定模板实参的过程被称为模板实参推断
关于模板类型参数的类型转换:
其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。
如果希望允许对函数实参进行正常的类型转换,我们可以将函数模板定义为两个类型参数:
long lng; compare(lng, 1024); // 错误:不能实例化compare(long, int) // 实参类型可以不同,但必须兼容 template <typename A, typename B> int flexibleCompare(const A& v1, const B& v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; }
如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
显式模板实参在尖括号中给出,位于函数名之后,实参列表之前。
显式模板实参按由左至右的顺序与对应的模板参数匹配;只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。
// 编译器无法推断T1,它未出现在函数参数列表中 template <typename T1, typename T2, typename T3> T1 sum(T2, T3); // T1是显式指定的,T2和T3是从函数实参类型推断而来的 auto val3 = sum<long long>(i, lng); // long long sum(int, long)
对于模板类型参数已经显式指定了的函数实参,可以进行正常的类型转换。
由于尾置返回出现在参数列表之后,它可以使用函数的参数:
// 尾置返回允许我们在参数列表之后声明返回类型 template <typename It> auto fcn(It beg, It end) -> decltype(*beg) { // 处理序列 return *beg; // 返回序列中一个元素的引用 }
组合使用remove_reference,尾置返回及decltype,我们就可以在函数中返回元素值的拷贝
// 为了使用模板参数的成员,必须用typename template <typename It> auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type { // 处理序列 return *beg; // 返回序列中一个元素的拷贝(非引用) }
标准类型转换模板
对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 |
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
template <typename T> int compare(const T&, const T&); // pf1指向实例int compare(const int&, const int&) int (*pf1)(const int&, const int&) = compare; // pf1中参数的类型决定了T的模板实参的类型。在本例中,T的模板实参类型为int
如果不能从函数指针类型确定模板实参,则产生错误:
// func的重载版本;每个版本接受一个不同的函数指针类型 void func(int(*)(const string&, const string&)); void func(int(*)(const int&, const int&)); func(compare); // 错误:使用compare的哪个实例?
我们可以通过使用显式模板实参来消除func调用的歧义:
// 正确:显式指出实例化哪个compare版本 func(compare<int>); // 传递compare(const int&, const int&)
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
从函数调用进行模板实参的类型推断时,编译器会应用正常的引用绑定规则;const是底层的,不是顶层的。
如果实参是const的,则T将被推断为const类型
当函数参数本身是const时,T的类型推断的结果不会是一个const类型。
C++语言在正常绑定规则之外定义了两个例外规则:
对于一个给定类型X:
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。
如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被绑定到一个左值;且如果实参是一个左值,则推断出的模板类型参数将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&)。
使用右值引用的函数模板通常使用以下方式来进行重载:
template <typename T> void f(T&&); // 绑定到非const右值 template <typename T> void f(const T&); // 左值和const右值
标准库是这样定义move的:
// 在返回类型和类型转换中也要用到typename template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); }
虽然不能隐式地将一个左值转换为右值引用,但我们可以用static_cast显式地将一个左值转换为一个右值引用。
如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值/右值属性将得到保持。
函数参数与其他任何变量一样,都是左值表达式。
forward
定义在头文件utility
中,与move
不同,forward
必须通过显式模板实参来调用。forward
返回该显式实参类型的右值引用。即,forward<T>
的返回类型是T&&
。
当用于一个指向模板参数类型的右值引用函数参数(T&&)时,forward会保持实参类型的所有细节。
template <typename F, typename T1, typename T2> void flip(F f, T1 &&t1, T2 &&t2) { f(std::forward<T2>(t2), std::forward<T1>(t1)); }
与std::move
相同,对std::forward
不使用using
声明是一个好主意。
如果涉及函数模板,则函数匹配规则会在以下几方面受到影响:
正确定义一组重载的函数模板需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解。
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。
一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包:
在一个模板参数列表中,class...
或typename...
指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。例如:
// Args是一个模板参数包;rest是一个函数参数包 // Args表示零个或多个模板类型参数 // rest表示零个或多个函数参数 template <typename T, typename... Args> void foo(const T &t, const Args& ... rest);
当我们需要知道包中有多少元素时,可以使用sizeof...
运算符。类似sizeof,sizeof...也返回一个常量表达式,而且不会对其实参求值:
template <typename ...Args> void g(Args ...args) { cout << sizeof...(Args) << endl; // 类型参数的数目 cout << sizeof...(args) << endl; // 函数参数的数目 }
可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
对于最后一个调用,两个函数提供同样好的匹配。但是,非可变参数模板比可变参数模板更特例化,因此编译器选择非可变参数的版本。
当定义可变参数版本的函数时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归。
对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展它。当扩展一个包时,我们还要提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(...)来触发扩展操作。
扩展中的模式会独立地应用于包中的每个元素。
我们可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数。
可变参数函数通常将它们的参数转发给其他函数。
// fun有零个或多个参数,每个参数都是一个模板参数类型的右值引用 template <typename... Args> void fun(Args&&... args) // 将Args扩展为一个右值引用的列表 { // work的实参既扩展Args又扩展args work(std::forward<Args>(args)...); }
一个模板特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型。
当我们特例化一个函数模板时,必须为原模版中的每个模板参数都提供实参。使用关键字template后跟一个空尖括号对(<>)。空尖括号指出我们将为原模板的所有模板参数提供实参:
// compare的特殊版本,处理字符数组的指针 template <> int compare(const char* const &p1, const char* const &p2) { return strcmp(p1, p2); } // 本例注意理解函数参数类型const char* const &
特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。
模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本(即,模板特例化版本必须出现在原模板的声明之后,任何使用模板实例的代码之前)。
我们可以向命名空间添加成员:
// 打开std命名空间,以便特例化std::hash namespace std { } // 关闭std命名空间;注意:右花括号之后没有分号
特例化类:
// 打开std命名空间,以便特例化std::hash namespace std { template <> // 我们正在定义一个特例化版本,模板参数为Sales_data struct hash<Sales_data> { // 用来扩散一个无序容器的类型必须要定义下列类型 typedef size_t result_type; typedef Sales_data argument_type; // 默认情况下,此类型需要== size_t operator()(const Sales_data& s) const; // 我们的类使用合成的拷贝控制成员和默认构造函数 }; size_t hash<Sales_data>::operator()(const Sales_data& s) const { return hash<string>()(s.bookNo) ^ hash<unsigned>()(s.units_sold) ^ hash<double>()(s.revenue); } } // 关闭std命名空间;注意:右花括号之后没有分号
默认情况下,为了处理特定关键字类型,无序容器会组合使用key_type
对应的实例化hash
版本和key_type
上的相等运算符。
我们只能部分特例化类模板,而不能部分特例化函数模板。
// 原始的、最通用的版本 template <class T> struct remove_reference { typedef T type; }; // 部分特例化版本,将用于左值引用和右值引用 template <class T> struct remove_reference<T&> // 左值引用 { typedef T type; }; template <class T> struct remove_reference<T&&> // 右值引用 { typedef T type; };
部分特例化版本的模板参数列表是原始模板的参数列表的一个子集或者是一个特例化版本。
我们可以只特例化特定成员函数而不是特例化整个模板:
template <typename T> struct Foo { Foo(const T &t = T()) : mem(t) { } void Bar() { /* ... */ } T mem; // Foo的其他成员 }; template<> // 我们正在特例化一个模板 void Foo<int>::Bar() // 我们正在特例化Foo<int>的成员Bar { // 进行应用于int的特例化处理 }