面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。
不同之处在于:OOP 能处理类型在程序运行之前都未知的情况;而在泛型编程中,在编译时就能获知类型了。
模板是C++中泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。
定义模板
在实际中,可能需要处理多个不同类型变量的同一类问题,可以定义一个通用的函数模板,而不是为每个类型都定义个新函数。一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。
template <typename T>
int compare(const T &v1, const T &v2){if(v1 < v2) return -1;if(v2 < v1) return 1;return 0;
}
函数模板
模板定义以关键字template
开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用小于号(<
)和大于号(>
)包围起来。
在模板定义中,模板参数列表不能为空。
模板参数列表的作用很像函数参数列表。函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时,调用者提供实参来初始化形参。
类似的,模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们(隐式地或显式地)指定模板实参,将其绑定到模板参数上。
实例化函数模板
编译器用推断出的模板参数来为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。
//实例化出int compare (const int&,const int&)
cout <<compare(1,0)<<endl;//T为int
//实例化出int compare (const vector<int>&,const vector<int>& )
vector<int> vec1{1,2,3),vec2(4,5,6);
cout<<compare(vec1,vec2)<< endl;//T为vector<int>
模板类型参数
通常可以将类型参数看作类型说明符,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
//正确:返回类型和参数类型相同
template <typename T>T foo(T* p){T tmp = *p;// tmp的类型将是指针p指向的类型// ...return tmp;
}
模板参数前必须使用关键字 class
或 typename
,两个含义相同,一个参数列表可以同时使用两个关键字。
template <typename T, class U> calc (const T&, const U&);
非类型模板参数
除了定义类型参数,还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。
通过一个特定的类型名而非关键字 class
或 typename
来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。
template<unsigned N, unsigned M>
int compare(const char (&p1)[N],const char (&p2)[M]){return strcmp (p1,p2);
}
//编译器会使用字面常量的大小来代替N和M,从而实例化模板。
非类型模板参数的模板实参必须是常量表达式。
inline
和 constexpr
的函数模板
函数模板可以声明为inline
或constexpr
的,如同非模板函数一样。inline
或constexpr
说明符放在模板参数列表之后,返回类型之前:
//正确:inline说明符跟在模板参数列表之后
template <typename T> inline T min (const T&,const T&);
//错误:inline说明符的位置不正确
inline template <typename T> T min (const T&,const T&);
编写类型无关的代码
编写泛型代码有两个重要原则:
1.模板中的函数参数应该是 const
的引用。引用保证了函数可以用于不能拷贝的类型,如unique_ptr
, IO
类型。
2.函数体中的条件判断仅使用 <
比较运算。
模板程序应该尽量减少对实参类型的要求。
模板编译
当编译器遇到一个模板定义时,它并不生成代码。只有当实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用(而不是定义)模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。
函数模板和类模板成员函数的定义通常放在头文件中。因为编译器需要知道函数模板或类模板成员函数的完整定义才能进行实例化。
模板包含两种名字:
- 那些不依赖于模板参数的名字
- 那些依赖于模板参数的名字
大多数编译错误在实例化期间报告
第一个阶段是编译模板本身时。在这个阶段,编译器通常不会发现很多错误。编译器可以检查语法错误,例如忘记分号或者变量名拼错等,仅此而已。
第二个阶段是编译器遇到模板使用时。在此阶段,编译器仍然没有很多可检查的。对于函数模板调用,编译器通常会检查实参数目是否正确。它还能检查参数类型是否匹配。对于类模板,编译器可以检查用户是否提供了正确数目的模板实参,但也仅限于此了。
第三个阶段是模板实例化时。只有这个阶段才能发现类型相关的错误。依赖于编译器如何管理实例化,这类错误可能在链接时才报告。
类模板
类模板是用来生成类的蓝图的。
与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。
为了使用类模板,必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
定义类模板
template <typename T> class Blob{};
实例化类模板
当使用一个类模板时,必须提供额外信息。这些额外信息是显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板实参来实例化出特定的类。
Blob<int> ia;//空Blob<int>
Blob<int> ia2 = {0,1,2,3,4};//有5个元素的Blob<int>
一个类模板的每个实例都形成一个独立的类。
类模板的成员函数
与其他任何类相同,既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数 。
类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以关键字template
开始,后接类模板参数列表。
类模板成员函数的实例化
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
在类代码内简化模板类名的使用
使用一个类模板类型时必须提供模板实参。例外:在类模板自己的作用域中,可以直接使用模板名而不提供实参。
处于一个类模板的作用域中时,编译器处理模板自身引用时就好像已经提供了与模板参数匹配的实参一样。
在类模板外使用类模板名
在类模板外定义其成员时,必须记住,并不在类的作用域中,直到遇到类名才表示进入类的作用域。
//后置:递增/递减对象但返回原值
template <typename T>
BlobPtr<T> B1obPtr<T>::operator++(int){//此处无须检查;调用前置递增时会进行检查BlobPtr ret *this; //保存当前值++*this;//推进一个元素;前置++检查递增是否合法return ret; //返回保存的状态
}
//由于返回类型位于类的作用域之外,必须指出返回类型是一个实例化的BlobPtr,它所用类型与类实例化所用类型一致
在一个类模板的作用域内,可以直接使用模板名而不必指定模板实参。
类模板和友元
如果一个类模板包含一个非模板友元,则该友元可以访问该模板的所有实例。
如果友元也是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友好关系
类模板与另一个(类或函数)模板间友好关系的最常见的形式是建立对应实例及其友元间的友好关系。
//前置声明,在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>&);// 将访问权限授予同类型实例化的 ==。
};Blob<char> ca; // BlobPtr<char>和 operator==<char>都是本对象的友元
Blob<int> ia; // Blobptr<int>和operator==<int>都是本对象的友元
通用和特定的模板友好关系
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元。
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
//前置声明,在将模板的一个特定实例声明为友元时要用到
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的前置声明
};
模板类型别名
可以定义一个 typedef
来引用实例化的类,但不能引用一个模板。
但是,新标准允许我们为类模板定义-一个类型别名:
template<typename T> using twin = pair<T,T>;
twin<string> authors;// authors是一个pair<string,string>
//将twin定义为成员类型相同的pair的别名。
类模板的 static 成员
与任何其他类相同,类模板可以声明static成员。
如果类模板定义了 static 成员,那么模板的每个实例都有自己独有的 static 成员实例。
template <typename T> class Foo{
public:static std::size_t count() { return ctr; 〕//其他接口成员
private:static std::size_t ctr;//其他实现成员//Foo是一个类模板,它有一个名为count的public static成员函数和一个名为ctr的private static数据成员。每个 Foo 的实例都有其自己的 static 成员实例。
与任何其他static
数据成员相同,模板类的每个static
数据成员必须有且仅有一个定义。
但是,类模板的每个实例都有一个独有的static
对象。
因此,与定义模板的成员函数类似,要将static
数据成员也定义为模板。
template <typename T>
size_t Foo<T>::ctr = 0;//定义并初始化ctr
类似任何其他成员函数,一个static成员函数只有在使用时才会实例化
模板参数
类似函数参数的名字,一个模板参数的名字也没有什么内在含义。
模板参数与作用域
模板参数遵循普通的作用域规则。一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。与任何.其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是,与大多数其他上下文不同,在模板内不能重用模板参数名。
typedef double A;
template <typename A,typename B> void f(A a, B b){A tmp = a;// tmp 的类型为模板参数A的类型,而非doubledouble B;//错误:重声明模板参数B
由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次。
//错误:非法重用模板参数名V
template <typename v, typename v>//...
模板声明
模板声明必须包含模板参数,与函数参数相同,声明中的模板参数的名字不必与定义中相同(与函数形参类似)。
使用类的类型成员
默认情况下,C+语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。当希望通知编译器一个名字表示类型时,必须使用关键字 typename
而不能使用 class
template <typename T> typename T::value_type top(const T& c);
默认模板实参
与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
模板默认实参与类模板
无论何时使用一个类模板,都必须在模板名之后接上尖括号。尖括号指出类必须从一个模板实例化而来。特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对。
template <class T = int> class Numbers { //T默认为int public:Numbers(T v=0): val(v){};//对数值的各种操作private:private:T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average precision;//空<>表示我们希望使用默认类型
成员模板
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板。
成员模板不能是虚函数。
因为虚函数的动态多态性需要在编译时就确定函数的签名,以便在运行时进行动态绑定。但是,模板的实例化是在编译时发生的,编译器需要知道模板的参数类型才能生成相应的代码。因此,成员模板无法与虚函数的动态多态性相匹配。
class Base {
public:// 这里是成员模板,无法声明为虚函数template <typename T>virtual void Print(T value) {std::cout << "Base: " << value << std::endl;}
};class Derived : public Base {
public:// 错误:成员模板不能被声明为虚函数template <typename T>virtual void Print(T value) override {std::cout << "Derived: " << value << std::endl;}
};int main() {Base* ptr = new Derived();ptr->Print(5); // 编译错误,无法进行动态绑定return 0;
}
与任何其他模板相同,成员模板也是以模板参数列表开始的。
控制实例化
相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。可以通过显式实例化来避免额外开销。
extern template declaration;//实例化声明
template declaration;//实例化定义
对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。
在一个类模板的实例化定义中,所用类型必须能用于模板的所有成员函数。
模板实参推断
对于函数模板,编译器利用调用中的函数实参来确定其模板参数。从函数实参来确定模板实参的过程称为模板实参推断。
模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配。
类型转换与模板类型参数
如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有很有限的几种类型转换会自动地应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有 const
转换及数组或函数到指针的转换。
顶层const
无论是在形参中还是在实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括如下两项:
- const 转换:可以将一个非
const
对象的引用(或指针)传递给一个const
的引用(或指针)形参。 - 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指。
其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。
template <typename T>T fobj(T,T); //实参被拷贝
template <typename T>T fref (const T&,const T&); // 引用
string s1("a value");
const string s2("another value");
fobj(s1,s2); // 调用fobj(string,string); const被忽略
fref(sl,s2); //调用fref(const string&,const string&)//将s1转换为const是允许的
int a[10],b[42];
fobj(a,b); //调用f(int*, int*)
fref(a,b); //错误:数组类型不匹配
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const
转换及数组或函数到指针的转换。
一个模板类型参数可以用作多个函数形参的类型。
如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
函数模板显式实参
在某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。
对于用普通类型定义的函数参数,允许进行正常的类型转换,出于同样的原因,对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换:
long lng;
compare(lng, 1024); //错误:模板参数不匹配
compare<long>(lng, 1024); //正确:实例化compare (long, long)
compare<int>(lng, 1024); //正确:实例化compare(int, int)
尾置返回类型与类型转换
当希望用户确定返回类型时,用显式模板实参表示模板函数的返回类型是很有效的。但在其他情况下,要求显式指定模板实参会给用户增添额外负担,而且不会带来什么好处。
template <typename It>
??? &fcn(It beg, It end){//处理序列return *beg;//返回序列中一个元素的引用
}
// 并不知道返回结果的准确类型,但知道所需类型是所处理的序列的元素类vector<int> vi= {1,2,3,4,5};
Blob<string> ca = {"hi","bye"};
auto &i = fcn(vi.begin(),vi.end());// fcn应该返回int&
auto &s = fcn(ca.begin(),ca.end());// fcn应该返回string&//此例中,已知函数应该返回*beg,而且知道我们可以用decltype (*beg)来获取此表达式的类型。
//但是,在编译器遇到函数的参数列表之前,beg 都是不存在的。为了定义此函数,必须使用尾置返回类型。
//必须使用尾置返回类型
函数指针和实参推断
当用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
模板实参推断和引用
template <typename T>void f(T &p);
其中函数参数p是一个模板类型参数T的引用,
非常重要的两点:1. 编译器会应用正常的引用绑定规则;2. const 是底层的,不是顶层的。
从左值引用函数参数推断类型
当一个函数参数是模板类型参数的一个普通(左值)引用时(即,形如T&
),绑定规则告诉我们,只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。实参可以是const
类型,也可以不是。如果实参是const
的,则T将被推断为const
类型:
template <typename T> void f1(T&);//实参必须是一个左值
//对 f1 的调用使用实参所引用的类型作为模板参数类型
f1(i); // i 是一个 int; 模板参数类型 T 是int
f1(ci); // ci 是一个 const int;模板参数T是 const int
f1(5); //错误:传递给一个&参数的实参必须是一个左值
如果一个函数参数的类型是const T&
,正常的绑定规则告诉我们可以传递给它任何类型的实参——一个对象(const
或非 const
)、一个临时对象或是一个字面常量值。当函数参数本身是const
时,T
的类型推断的结果不会是一个const
类型。const
已经是函数参数类型的一部分:因此,它不会也是模板参数类型的一部分。
从右值引用函数参数推断类型
当一个函数参数是一个右值引用(即,形如T&&
)时,正常绑定规则告诉我们可以传递给它一个右值。当我们这样做时,类型推断过程类似普通左值引用函数参数的推断过程。推断出的T的类型是该右值实参的类型。
引用折叠和右值引用参数
通常不能将一个右值引用绑定到一个左值上。但是,C++语言在正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个例外规则是move这种标准库设施正确工作的基础。
第一个例外规则影响右值引用参数的推断如何进行:当我们将一个左值(如i
)传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&
)时,编译器推断模板类型参数为实参的左值引用类型;
第二个例外规则是:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即,对于一个给定类型x
:1. X& &、X& &&
和X&& &
都折叠成类型x&
; 2.类型X&& &&
折叠成 X&&
。
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。
如果一个函数参数是指向模板参数类型的右值引用(如,T&&
),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&
).
转发
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,需要保持被转发实参的所有性质,包括实参类型是否是const
的以及实参是左值还是右值。
通过将一个函数参数定义为一个指向模板类型参数的右值引用,可以保持其对应实参的所有类型信息。而使用引用参数(无论是左值还是右值)使得我们可以保持const
属性,因为在引用类型中的const
是底层的。如果我们将函数参数定义为T1&&
和T2&&
,通过引用折叠就可以保持翻转实参的左值/右值属性。
如果一个函数参数是指向模板类型参数的右值引用(如T&&
),它对应的实参的const
属性和左值/右值属性将得到保持。
在调用中使用std::forward
保持类型信息
当用于一个指向模板参数类型的右值引用函数参数(T&&)
时,forward
会保持实参类型的所有细节。
template <typename Type> intermediary (Type &&arg)
{finalFcn (std::forward<Type>(arg));// ...
}
重载与模板
函数模板可以被另一个模板或一个普通非模板函数重载。与往常一样名字相同的函数必须具有不同数量或类型的参数。
如果涉及函数模板,则函数匹配规则会在以下几方面受到影响:
1.对于一个调用,其候选函数包括所有模板实参推断)成功的函数模板实例;
2.候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板;
3.与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的;
4.与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:
-如果同样好的函数中只有一个是非模板函数,则选择此函数。
-如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
-否则,此调用有歧义。
正确定义一组重载的函数模板需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解。
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
在定义任何函数之前,要记得声明所有重载的函数版本。
可变函数模板
一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数。
我们用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class...
或typename.….
指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
// Args是一个模板参数包;rest是一个函数参数包
// Args表示零个或多个模板类型参数
// rest表示零个或多个函数参数
template <typename T,typename... Args>
void foo(const T &t,const Args& ... rest);
sizeof… 运算符
当需要知道包中有多少元素时,可以使用sizeof...
运算符。
template<typename ... Args> void g(Args ... args){cout << sizeof. ..(Args)<<endl; //类型参数的数目cout << sizeof. ..(args)<< endl; //函数参数的数目
}
包拓展
对于一个参数包,除了获取其大小外,能对它做的唯一的事情就是扩展(expand)它。
当扩展一个包时,还要提供用于每个扩展元素的模式(pattern)。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。通过在模式右边放一个省略号(…)来触发扩展操作。
template <typename T,typename. .. Args>
ostream &
print (ostream &os,const T &t,const Args&...rest) //扩展Args
{os << t << ",";return print (os, rest...); //扩展rest
}
//第一个扩展操作扩展模板参数包,为 print 生成函数参数列表。
//第二个扩展操作出现在对 print 的调用中。此模式为 print 调用生成实参列表。
扩展中的模式会独立地应用于包中的每个元素。
重要术语
类模板 模板定义,可从它实例化出特定的类。类模板的定义以关键宇template
开始,后跟尖括号对<利和>,其内为个用逗号分隔的个或多个模板参数的列表,随后是类的定义。
默认模板实参 一个类型或一个值、当用户未提供对应模板实参时,模板会使用它。
显式实例化 一个声明,为所有模板参数提供了显式实参。用来指导实例化过程。如果声明是extern
的,模板将不会被实例化;否则,模板将利用指定的实参进行实例化。对每个extern
模板声明,在程序中某处必须有个非extern
的显式实例化。
函数参数包 表示零个或多个函数参数的参数包。
实例化 编译器处理过程,用实际的模板实参来生成模板的一个特殊实例,其中参数被替换为对应的实参。当函数模板被调用时,会自动根据传递给它的实参来实例化。而使用类模板时,则需要我们提供显式模板实参。
参数包 表示零个或多个参数的模板或函数参数。
包扩展 处理过程,将一个参数包替换为其中元素的列表。
模式 定义了扩展后参数包中每个元素的形式。
模板参数包 表示零个或多个模板参数的参数包。
可变参数模板 接受可变数目模板实参的模板。模板参数包用省略号指定(如 class...
、typename...
或type-nam...
)