模板是C++中泛型编程的基础,一个模板就是创建一个类或者函数的蓝图或者说公式。
C++模板分为函数模板和类模板。C++根据调用模板时传入的具体类型来生成相应类型的具体函数或者类。
类模板则可以是整个类是个模板,类的某个成员函数是个模板,以及类本身和成员函数分别是不同的模板。
1.函数模板
函数模板以关键字template开始,后接尖括号括起来的模板参数列表,模板参数列表不允许是空的,也即模板参数至少有一个或多个,多个之间使用逗号分割。
模板参数表示的是函数中用到的类型或者是一个值。当我们使用模板时,根据提供的实参推断出实参的类型,该类型即被用于绑定到模板参数,这个过程被叫做模板的实例化,相应的,生成的版本叫做模板的实例。
模板参数表示的是类型,该类型可以用于制定函数的返回类型或者函数的参数类型,也可以用于函数体内变量声明定义等。
对于每个模板参数列表中的每个类型参数,其前面必须加上关键字typename或class。关键字typename和class之间没有区别。
除了定义模板的类型参数,还可以定义一种非类型的参数。非类型参数不表示一种类型,而是表示一个值。非类型参数也不使用关键字typename和class,而是使用具体的类型来指定。
非类型参数在模板实例化时,被用户或者编译器推断出的值所代替,该值必须是常量表达式。(常量表达式是指值不会改变,并且编译期间就能计算得出结果的表达式,字面值属于常量表达式,常量表达式初始化的const对象也是常量表达式)。
例如:
template <unsigned N, unsigned M> int fun(const char (&p1)[N], const char (&p2)[M]) { return strcmp(p1, p2); }
这里的N,M将会被我们调用fun时传入的实参的值替代:
fun("hi", "mom");
编译器会使用字面常量的大小来替代N,M,并在字符串字面值末尾插入一个空字符作为终止标记,最终实例化为:
int fun(const char (&p1)[3], const char (&p2)[4]);
非类型参数可以是整形,也可以是指针或者引用。条件是整形是常量表达式,而指针和引用必须是关联static对象。
模板可以声明为inline和constexpr的。
当编译器对代码进行编译时,在源码模板定义部分,编译器并没有实际去生成相应的模板代码,只有在使用模板实例化一个具体的版本时,编译器才生成相应的代码。
2.类模板
与函数模板不同,类模板不能为其模板类型参数进行推断。为什么不能为类模板推断类型参数?因为定义类对象的时候,有可能无法提供足够的类型来让编译器进行推断,比如 vector v;这里仅仅定义了一个vector对象,没有提供任何额外的信息让编译器来推断模板参数,所以类模板不为其模板类型参数进行推断,必须我们在实例化时显式指明。
类模板的名字不是一个类,也即不是自定义的一个class,而是一个生成class的说明模板。因此使用模板生成的类,必定是带模板类型实参的class,所以一个实例化的class必然有<>来指明类型参数。
类模板的成员函数可以在内部定义,这样是隐式内联。也可以在外部定义。类模板的成员函数是一个普通的成员,而不是模板。虽然在外部定义时,成员函数需要以关键字template开始,并且后接模板参数列表,但这不表示该成员函数是个模板。之所以需要关键字template和模板参数列表,是因为成员函数所属的类在实例化时,会具体绑定到一个特定的类型上,成员函数也需要相应被动地绑定到该类型。
类模板的成员函数只有被用到的时候才会进行实例化,如果没有被用到,就不会实例化,如同类中定义的成员函数一样,如果不会被用到,那么可以只声明而不定义它。
当我们在类模板作用域内进行模板成员的定义时,可以省略模板实参。
template <typename T> class BlobPtr { public: BlobPtr& operator++(); //无需说明具体类名BlobPtr<T> BlobPtr& operator--(); //无需说明具体类名BlobPtr<T> BlobPtr operator--(int); //无需说明具体类名BlobPtr<T> bool empty() { return data->empty(); } size_t size(); private: std::shared_ptr<std::vector<T>> data; }; template <typename T> size_t BlobPtr<T>::size() //类作用域外,需说明具体类名BlobPtr<T> { return data->size(); }
当我们在类模板的外面定义成员函数时,必须以关键字template开始,后接类模板参数列表。
这里为什么类成员函数size( )前要加模板参数列表<T>?template<typyname T>不已经指明了类型了吗?
前面已经说过:类模板的成员函数是一个普通的成员,而不是模板。这里的template <typename T>是说明size( ) 所属的BlobPtr是个模板类,并且BlobPtr
本身也不是一个类名,是类模板名,真正的类名是BlobPtr<T>,说白了,其实是在具体指明类作用域。所以在类外定义成员函数时,成员函数的参数列表对模板参数是可以省略的。
因此在类模板外面定义普通非模板成员函数时,首先用template <typename T>说明是一个类模板,然后再具体说明该成员函数是哪个类中的成员。后面会看到,如果模板类的成员函数也是一个模板,那么就需要分别各自说明类的模板和函数的模板。
3.类模板和友元
如果一个类模板包含一个非模板友元,则友元可以访问该类模板的所有实例。
如果类和友元都是模板,则类实例可以对友元所有实例授权,也可以只授权给特定实例。
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
在C++11中,我们也可以模板类型参数声明成友元。例如:
template <typename Type> class Bar { friend Type; // ... };
新标准也允许我们为类模板定义一个类型别名:
template <typename T> using twin = pair<T, T>; twin<string, string> authors; //authors是一个pair<string, string>
模板参数遵循普通的作用域规则。与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字,需要注意的是,在模板内不能重用模板参数名。例如:
typedef double A; template <typename A, typename B> void f(A a, B b) { A tmp = a; //类型A隐藏外部typedef double A double B; //错误,不允许重用模板类型参数 }
模板声明必须包含模板参数。与函数参数相同,声明中的模板参数的名字不必与定义中相同。
对于一个给定的模板的声明和定义必须有相同数量和种类的参数。
关于typename可以用做模板参数的关键字,也可以用来指示类型还是变量名。具体可以参考这篇文章:http://feihu.me/blog/2014/the-origin-and-usage-of-typename/
4.类模板的static成员
与普通类一样,模板类也可以拥有static成员,如下:
template <typename T> class Foo { public: static std::size_t count() { return ctr; } //声明并定义 private: static std::size_t ctr; //声明,尚未定义 };
上面这段代码,Foo是一个类模板,它实例化后的类有一个count的静态成员函数和一个静态ctr数据成员。
类的static数据成员有且只有一个定义,类模板也是如此。因此,我们将需要在类模板外定义ctr数据成员,类模板外定义静态数据成员的格式是template关键字开始,后跟模板参数列表,如下:
template <typename T> size_t Foo<T>::ctr = 0;
上面代码中类名是带模板参数的,因为实例化后的静态数据成员是具体属于某一个类的,而一个具体类则是带模板实参的,仅有类名则只是一个类模板的名字。
在新标准中,我们还可以为函数模板和类模板提供默认模板实参,对于类模板,当使用默认实参时,只需使用空的尖括号<>来表示即可。
5.成员模板
一个普通类或者类模板可以包含一个模板的成员函数。这种成员被称为成员模板。成员模板不允许是虚函数。
对于类模板,其类和成员有各自独立的模板参数。
与类模板的普通成员函数不同,成员模板是函数模板。当我们在类模板外面定义实现成员模板时,要同时提供类模板和成员模板的参数列表。其中,类模板的参数列表在前,成员模板的在后。例如:
template <typename T> class Blob { template <typename It> Blob(It b, It e); // ... }; template <typename T> //类模板的参数列表 template <typename It> //成员模板参数列表 Blob<T>::Blob(It b, It e):data(std::make_shared<std::vector<T>>(b, e)) { //constructor }
对于含成员模板的类模板,在实例化时,需要同时提供类模板实参和成员模板实参,对于类模板实参,则是显式提供,对于成员模板的实参,则是自动推断。
6.控制实例化
模板只有被用到时,编译器才会根据实参进行实例化实参相应的代码,在不同源文件中提供相同实参实例化同一模板时,将会在不同文件中重复生成相同的实例,每个源文件中都会有一个实例。大系统中,这会导致严重的额外开销。
C++11新标准中,可以使用显式实例化来避免这种额外开销。方法是使用实例化声明和实例化定义。
形如:
extern template declaration; //实例化声明 template declaration; //实例化定义
下面是实际的例子
extern template class Blob<string>; // 声明 template int compare(const int&, const int&); // 定义
上面的代码中,第一行是声明,第二行是定义。需要注意的是,模板的实例化控制的声明和定义是配套使用的。
当编译器遇到extern模板声明时,编译器就不再实例化,它会去程序的其他地方寻找实例,可以多次声明,但只能一次定义。
由于编译器会在使用模板时自动实例化,因此extern声明必须在任何使用当前模板前面声明。
当编译器遇到一个模板的实例化控制的定义时,编译器将会进行实例化,以生成代码。
类模板的实例化定义会实例化模板的所有成员,而不是用到哪个成员才实例化哪个成员。
7.