C++模板相关知识点总结
1:在 C++ 中,模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。
2:模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔,模板形参表不能为空:
template <typename T> int compare(const T &v1, const T &v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; }
3:模板形参表示可以在类或函数的定义中使用的类型或值。模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。类型形参跟在关键字 class 或 typename 之后定义,这里 class 和 typename 没有区别。非类型形参跟在类型说明符之后声明。
4:调用函数模板时,编译器将确定用什么类型代替每个类型形参,以及用什么值代替每个非类型形参。推导出实际模板实参后,编译器使用实参代替相应的模板形参产生编译该版本的函数。编译器承担了为我们使用的每种类型而编写函数的单调工作。
5:函数模板可以用与非模板函数一样的方式声明为 inline。inline说明符放在模板形参表之后、返回类型之前,不能放在关键字 template 之前。
6:与调用函数模板形成对比,使用类模板时,必须为模板形参显式指定实参: Queue<int> qi;
7:模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。
模板形参遵循常规名字屏蔽规则。与全局作用域中声明的对象、函数或类型同名的模板形参会屏蔽全局名字:
typedef double T; template <class T> T calc(const T &a, const T &b) { // tmp has the type of the template parameter T // not that of the global typedef T tmp = a; // ... return tmp; }
用作模板形参的名字不能在模板内部重用:
template <class T> T calc(const T &a, const T &b) { typedef double T; // error: redeclares template parameter T T tmp = a; // ... return tmp; }
8:在函数模板形参表中,关键字 typename 和 class 具有相同含义,可以互换使用,两个关键字都可以在同一模板形参表中使用:
template <typename T, class U> calc (const T&, const U&);
使用关键字 typename 代替关键字 class 指定模板类型形参也许更为直观。毕竟,可以使用内置类型(非类类型)作为实际的类型形参,而且,typename更清楚地指明后面的名字是一个类型名。但是,关键字 typename 是作为标准C++ 的组成部分加入到 C++ 中的,因此旧的程序更有可能只用关键字 class。
9:类中可以定义类型成员。如果要在函数模板内部使用这样的类型,必须告诉编译器我们正在使用的名字指的是一个类型。
必须显式地这样做,因为编译器(以及程序的读者)不能通过检查得知,由类型形参定义的名字何时是一个类型何时是一个值。例如:
template <class Parm, class U> Parm fcn(Parm* array, U value) { // If Parm::size_type is a type, then a declaration // If Parm::size_type is an object, then multiplication Parm::size_type * p; }
如果希望编译器将 size_type 当作类型,则必须显式告诉编译器这样做:
template <class Parm, class U> Parm fcn(Parm* array, U value) { typename Parm::size_type * p; // ok: declares p to be a pointer }
通过在成员名前加上关键字 typename 作为前缀,可以告诉编译器将成员当作类型。
10:在调用函数时非类型形参将用值代替,值的类型在模板形参表中指定。例如,下面的函数模板声明了 array_init 是一个含有一个类型模板形参和一个非类型模板形参的函数模板:
template <class T, size_t N> void array_init(T (&parm)[N]) { for (size_t i = 0; i != N; ++i) { parm[i] = 0; } }
当调用 array_init 时,编译器从数组实参计算非类型形参的值:
int x[42]; double y[10]; array_init(x); // instantiates array_init(int(&)[42] array_init(y); // instantiates array_init(double(&)[10]
11:编译模板时,编译器可能会在三个阶段中标识错误:
第一阶段是编译模板定义本身时。在这个阶段一般可以检测到诸如漏掉分号或变量名拼写错误一类的语法错误。
第二个错误检测时间是在编译器见到模板的使用时。在这个阶段,对于函数模板的调用,编译器可以检测到实参太多或太少,也可以检测到假定类型相同的两个实参是否真地类型相同;对于类模板,编译器可以检测提供的模板实参的正确数目。
第三个是在实例化的时候,只有在这个时候可以发现类型相关的错误,比如实参类型不支持模板中使用的操作等等。根据编译器管理实例化的方式,有可能在链接时报告这些错误。
12:模板在使用时将进行实例化,类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或赋值时实例化。
类模板不定义类型,只有特定的实例才定义了类型。特定的实例化是通过提供模板实参与每个模板形参匹配而定义的。当编写”Queue<int> qi”时,编译器自动创建名为 Queue<int> 的类。
使用函数模板时,编译器通常会为我们推断模板实参。比如:
compare(1, 0);
compare(3.14, 2.7);
上面两个语句实例化了 compare 的两个版本:一个用 int 代替 T,另一个用double 代替 T。
13:要确定应该实例化哪个函数,编译器会查看每个函数实参。如果相应函数形参声明为类型形参的类型,则编译器从实参的类型推断形参的类型,从函数实参确定模板实参的类型和值的过程叫做模板实参推断。
模板类型形参可以用作一个以上函数形参的类型。在这种情况下,模板类型推断必须为每个对应的函数实参产生相同的模板实参类型。如果推断的类型不匹配,则调用将会出错。比如,针对模板:template <typename T> int compare(const T& v1, const T& v2),使用下面的调用就是错误的实例化:
short si = 1;
compare(si, 1024);
调用 compare 时的实参类型不相同,从第一个实参推断出的模板类型是 short,从第二个实参推断出 int 类型,两个类型不匹配,所以模板实参推断失败。
如果 compare(int, int)是普通的非模板函数,则该调用会将short 实参提升为 int。但是因为 compare 是一个模板,对于模板,编译器只会执行两种转换:
A:const 转换:接受 const 引用或 const 指针的函数模板可以分别用非 const对象的引用或指针来调用,无须产生新的实例化。如果函数模板接受非引用类型,形参类型和实参都忽略 const,即,无论传递 const 或非 const 对象给接受非引用类型的函数模板,都使用相同的实例化。
B:数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。
比如:
template <typename T> T fobj(T, T); // arguments are copied template <typename T> T fref(const T&, const T&); // reference arguments string s1("a value"); const string s2("another value"); fobj(s1, s2); // ok: calls f(string, string), const is ignored fref(s1, s2); // ok: non const object s1 converted to const reference int a[10], b[42]; fobj(a, b); // ok: calls f(int*, int*) fref(a, b); // error: array types don't match; arguments aren't converted to pointers
第一种情况下,传递 string 对象和 const string 对象作为实参,即使这些类型不完全匹配,两个调用也都是合法的。在 fobj 的调用中,实参被复制,因此原来的对象是否为 const 无关紧要。在 fref 的调用中,形参类型是 const 引用,对引用形参而言,转换为 const 是可以接受的转换,所以这个调用也正确。
在第二种情况中,将传递不同长度的数组实参。fobj 的调用中,数组不同无关紧要,两个数组都转换为指针,fobj 的模板形参类型是 int*。但是,fref的调用是非法的,当形参为引用时,数组不能转换为指针,a 和b 的类型不匹配,所以调用将出错。
类型转换的限制只适用于类型为模板形参的那些实参。用普通类型定义的形参还可以使用常规转换,比如这样的函数模板:template <class Type> Type sum(const Type &op1, int op2);该模板中,第一个形参 op1 具有模板形参类型,它的实际类型到函数使用时才知道;第二个形参 op2 的类型是int类型。对于这样的模板,像”sum(1024, 3.14);”这样的调用也是合法的,因为存在double到int的转换;
14:可以使用函数模板对函数指针进行初始化或赋值。此时,编译器使用指针的类型实例化具有适当模板实参的模板版本。比如:
template <typename T> int compare(const T&, const T&); // pf1 points to the instantiation int compare (const int&, const int&) int (*pf1) (const int&, const int&) = compare;
pf1 是一个函数指针,指向“接受两个 const int& 类型形参并返回 int值的函数”。将其赋值为模板compare时,函数形参的类型决定了 T 的模板实参的类型,因此,指针 pf1 引用的是将 T 绑定到 int 的实例化。
如果不能从函数指针类型为每个模板形参确定唯一的类型或值,就会出错。比如:
// overloaded versions of func; each take a different function pointer type void func(int(*) (const string&, const string&)); void func(int(*) (const int&, const int&)); func(compare); // error: which instantiation of compare?
查看 func 的形参类型不可能确定模板实参的唯一类型,该调用会产生一个编译时(或链接时)错误。
15:有些函数模板还会指定模板形参为返回值类型,比如:
// T1 cannot be deduced: it doesn't appear in the function parameter list template <class T1, class T2, class T3> T1 sum(T2, T3);
这种情况的函数模板,调用时没有实参的类型可用于推断 T1 的类型。因此,调用者必须在每次调用 sum 时为该形参显式提供实参:
// ok T1 explicitly specified; T2 and T3 inferred from argument types long val3 = sum<long>(i, lng); // ok: calls long sum(int, long)
这一调用显式指定 T1 的类型,编译器从调用中传递的实参推断 T2 和 T3的类型。
显式模板实参从左至右对应模板形参相匹配,第一个模板实参与第一个模板形参匹配,以此类推。若可以从函数实参推断出模板形参,则结尾(最右边)形参的显式模板实参可以省略。
如果模板是这样的话:
// poor design: Users must explicitly specify all three template parameters template <class T1, class T2, class T3> T3 alternative_sum(T2, T1);
因为第三个模板形参T3表示返回类型,因此调用函数模板时,比如提供T3的显式模板实参,进而需要提供T1和T2的模板实参。
可以使用显式模板实参解决上面函数指针的二义性问题:
template <typename T> int compare(const T&, const T&); // overloaded versions of func; each take a different function pointer type void func(int(*) (const string&, const string&)); void func(int(*) (const int&, const int&)); func(compare<int>); // ok: explicitly specify which version of compare
16:编写模板时,应该将模板的所有定义都放在同一个头文件中。具体参考《模板编译模型》。
17:通常,当使用类模板的名字的时候,必须指定模板形参。这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。比如:
template <class Type> class Queue { public: // empty Queue Queue(): head(0), tail(0) { } // copy control to manage pointers to QueueItems in the Queue Queue(const Queue &Q): head(0), tail(0) { copy_elems(Q); } ... }
在上面模板类中的内部,默认构造函数和复制构造函数分别使用了缩写的形式。编译器推断,当我们引用类的名字时,引用的是同一版本。比如,复制构造函数的定义等价于:
Queue<Type>(const Queue<Type> &Q): head(0), tail(0) { copy_elems(Q); }
如果类模板中使用了其他的模板类,则编译器不会为类中使用的其他模板的模板形参进行这样的推断,比如:
template <class Type> class Queue { public: ... private: QueueItem<Type> *head; // pointer to first element in Queue QueueItem<Type> *tail; // pointer to last element in Queue }
这些声明指出,对于 Queue 类的给定实例化,head 和 tail 指向为同一模板形参实例化的 QueueItem 类型的对象。
18:类模板成员函数的定义具有如下形式:必须以关键字 template 开关,后接类的模板形参表;必须指出它是哪个类的成员;类名必须包含其模板形参。比如:
template <class Type> void Queue<Type>::push(const Type &val) { ... }
19:类模板的成员函数本身也是函数模板,需要进行实例化。与其他函数模板不同的是,在实例化类模板成员函数时,编译器不执行模板实参推断,相反,类模板成员函数的模板形参由调用该函数的对象的类型确定。例如,当调用 Queue<int> 类型对象的 push 成员时,实例化的 push 函数为:void Queue<int>::push(const int &val)。
对象的模板实参能够确定成员函数模板形参,这一事实意味着,调用类模板成员函数比调用普通函数模板更灵活。用模板形参定义的函数形参的实参允许进行常规转换。比如:
Queue<int> qi; // instantiates class Queue<int> short s = 42; qi.push(s); // ok: s converted to int and passed to push
类模板的成员函数只有为程序所用才进行实例化。如果某函数从未使用,则不会实例化该成员函数。
定义模板类型的对象时,该定义导致实例化类模板。定义对象也会实例化用于初始化该对象的任一构造函数,以及该构造函数调用的任意成员。
20:下面是一个非类型模板形参的例子:
template <int hi, int wid> class Screen { public: // template nontype parameters used to initialize data members Screen(): screen(hi * wid, '#'), cursor (0), height(hi), width(wid) { } // ... private: std::string screen; std::string::size_type cursor; std::string::size_type height, width; };
当用户定义 Screen 对象时,必须为每个形参提供常量表达式以供使用:
Screen<24,80> hp2621; // screen 24 lines by 80 characters
注意,非类型模板实参必须是编译时常量表达式。
21:在类模板中可以出现三种友元声明:
a:普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数。如:
template <class Type> class Bar { // grants access to ordinary, nontemplate class and function friend class FooBar; friend void fcn(); // ... };
FooBar 的成员和 fcn 函数可以访问 Bar 类的任意实例的private 成员和 protected 成员。
b:类模板或函数模板的友元声明,授予对友元所有实例的访问权。如:
template <class Type> class Bar { // grants access to Foo1 or templ_fcn1 parameterized by any type template <class T> friend class Foo1; template <class T> friend void templ_fcn1(const T&); // ... };
友元声明 Foo1 和 temp1_fcn1 模板的类型形参与模板类Bar本身的类型形参不同。所以,Foo1 的任意实例都可以访问 Bar 的任意实例的私有元素,类似地,temp_fcn1 的任意实例可以访问 Bar 的任意实例。
c:只授予对类模板或函数模板的特定实例的访问权的友元声明。如:
template <class T> class Foo3; template <class T> void templ_fcn3(const T&); template <class Type> class Bar { // each instantiation of Bar grants access to the // version of Foo3 or templ_fcn3 instantiated with the same type friend class Foo3<Type>; friend void templ_fcn3<Type>(const Type&); // ... };
这些友元定义了 Bar 的特定实例与使用同一模板实参的 Foo3 或 temp1_fcn3 的实例之间的友元关系。每个 Bar 实例有一个相关的 Foo3 和 temp1_fcn3 友元:
注意,当授予对给定模板的所有实例的访问权时候,在作用域中不需要存在该类模板或函数模板的声明。实质上,编译器将友元声明也当作类或函数的声明对待。
当想要限制对特定实例化的友元关系时,必须在友元声明之前声明类或函数模板。因此:
template <class T> class A; template <class T> class B { public: friend class A<T>; // ok: A is known to be a template friend class C; // ok: C must be an ordinary, nontemplate class template <class S> friend class D; // ok: D is a template friend class E<T>; // error: E wasn't declared as a template friend class F<int>; // error: F wasn't declared as a template };
22:任意类(模板或非模板)可以拥有本身为类模板或函数模板的成员,这种成员称为成员模板,成员模板不能为虚。比如:
template <class Type> class Queue { public: // construct a Queue from a pair of iterators on some sequence template <class It> Queue(It beg, It end):head(0), tail(0) { copy_elems(beg, end); } // replace current Queue by contents delimited by a pair of iterators template <class Iter> void assign(Iter, Iter); // rest of Queue class as before private: // version of copy to be used by assign to copy elements from iterator range template <class Iter> void copy_elems(Iter, Iter); };
上面的例子中,三个成员函数全都属于成员模板,构造函数、assign和copy_elems三个成员模板的模板形参与类模板Queue本身的模板形参并不相同,他们都表示接受接受一对迭代器类型的实参。
如果需要在类模板外部定义成员模板,则:
template <class T> template <class Iter> void Queue<T>::assign(Iter beg, Iter end) { destroy(); // remove existing elements in this Queue copy_elems(beg, end); // copy elements from the input range }
成员模板的定义必须包含类模板形参以及自己的模板形参。首先是类模板形参表,后面接着成员自己的模板形参表。
成员模板只有在程序中调用时才实例化。成员模板有两种模板形参:由类定义的和由成员模板本身定义的。类模板形参由调用函数的对象的类型确定,成员定义的模板形参的行为与普通函数模板一样。这些形参都通过常规模板实参推断而确定。比如:
short a[4] = { 0, 3, 6, 9 }; // instantiates Queue<int>::Queue(short *, short *) Queue<int> qi(a, a + 4); // copies elements from a into qi vector<int> vi(a, a + 4); // instantiates Queue<int>::assign(vector<int>::iterator, vector<int>::iterator) qi.assign(vi.begin(), vi.end());
23:类模板可以像任意其他类一样声明 static 成员,每个实例化有自己的 static 成员。
24:某些情况下,通用模板定义对于某个类型可能是完全错误的。比如下面的compare 函数模板:与 C 风格字符串一起使用时,就会出现问题:
template <typename T> int compare(const T &v1, const T &v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; }
如果用两个 const char* 实参调用这个模板定义,函数将比较指针值,而不是比较两个字符串。
为了能够将 compare 函数用于字符串,必须提供一个知道怎样比较 C 风格字符串的特殊定义。这个版本是特化的,对用户而言,调用特化函数或使用特化类,与使用从通用模板实例化的版本无法区别。
模板特化(template specialization)是这样的一个定义,该定义中一个或多个模板形参的实际类型或实际值是指定的。特化的形式如下:
a:关键字 template 后面接一对空的尖括号(<>);
b:再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;
c:函数形参表;
d:函数体。
比如:特化后的compare函数模板定义如下:
template <> int compare<const char*>(const char* const &v1, const char* const &v2) { return strcmp(v1, v2); }
该特化版本的compare,类型实参为const char *,用于比较两个字符串数组。函数实参为”const char* const &”类型,也就是绑定到指向const字符的指针的const引用(该引用绑定的指针不能再指向其他字符,它指向的字符也不可以修改)。
模板特化声明看起来与定义很像,但省略了函数体:
template<> int compare<const char*>(const char* const&, const char* const&);
声明必须总是包含空模板形参说明符,即 template<>,而且,还必须包含函数形参表。如果可以从函数形参表推断模板实参,则不必显式指定模板实参,比如上面的声明还可以为下面的形式:
template<> int compare(const char* const&, const char* const&);
当调用特化模板的时候,对实参类型不应用转换,实参类型必须与特化版本函数的形参类型完全匹配,如果不完全匹配,编译器将为实参从模板定义实例化一个实例。
如果程序由多个文件构成,模板特化的声明必须在使用该特化的每个文件中出现。不能在一些文件中从模板定义实例化一个函数模板,而在其他文件中为同一模板实参集合特化该函数模板。
在能够声明或定义特化之前,它所特化的模板的声明必须在作用域中。类似地,在调用模板的这个版本之前,特化的声明必须在作用域中。特化出现在对该模板实例的调用之后是错误的。
对具有同一模板实参集的同一模板,程序不能既有显式特化又有实例化。
25:下面是一个特化类模板的例子。注意,特化可以定义与模板本身完全不同的成员:
template<typename T> class testtemp { public: testtemp(const T &a):m_value(a){} void display() { std::cout << m_value << std::endl; } private: T m_value; }; template<> class testtemp<int> { public: testtemp(const int &a):m_value2(a){} void display2() { std::cout << m_value2 << std::endl; } private: int m_value2; };
将类型形参绑定到int类型的特化版本的testtemp,它的成员与原始的testtemp模板的成员并不相同,这是没有问题的。
testtemp<short> t1(1); t1.display(); testtemp<int> tt(4); tt.display2();
上面的调用中,一个是将testtemp模板形参绑定到short类型的实例化,一个是使用特化版本。结果分别是打印出1和4.
如果是在类特化外部定义成员时,成员之前不能加 template<> 标记:
void testtemp<int>::display2() { std::cout << m_value2 << std::endl; }
如果加上了 template<> 标记,则会报编译错误。
除了特化整个模板之外,还可以只特化某个成员函数。比如:
template<> void testtemp<int>::display() { std::cout << "this is display: " << m_value << std::endl; }
现在,类类型 testtemp<int> 将从通用类模板定义实例化而来,而display函数例外。调用 testtemp<int> 对象的display函数时,将调用特化版本:
testtemp<short> t1(1); t1.display(); testtemp<int> tt(4); tt.display();
上面的调用将实例化两种类型,一个是将模板形参绑定到short类型的testtemp对象,它调用display函数时,使用的是通用实例化版本;另一个是将模板形参绑定到int类型的testtemp对象,它调用display时,使用的是特化版本。结果如下:
1 this is display: 4
26:如果类模板有一个以上的模板形参,可以使用类模板的部分特化,特化某些模板形参而非全部。比如:
template <class T1, class T2> class some_template { // ... }; // partial specialization: fixes T2 as int and allows T1 to vary template <class T1> class some_template<T1, int> { // ... };
类模板的部分特化本身也是模板。部分特化的定义看来像模板定义,定义以关键字 template 开头,接着是由尖括号(<>)括住的模板形参表。部分特化的模板形参表是对应的类模板定义形参表的子集。some_template 的部分特化只有一个名为 T1 的模板类型形参,第二个模板形参 T2 的实参已知为 int。部分特化的模板形参表只列出未知模板实参的那些形参。
部分特化的定义与通用模板的定义完全不会冲突。部分特化可以具有与通用类模板完全不同的成员集合。类模板成员的通用定义永远不会用来实例化类模板部分特化的成员。
27:函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。
如果重载函数中既有普通函数又有函数模板,确定函数调用的步骤如下:
(1):为这个函数名建立候选函数集合,包括:
a:与被调用函数名字相同的任意普通函数;
b:任意函数模板实例化,在其中,模板实参推断发现了与调用中所用函数实参相匹配的模板实参。
(2):确定哪些普通函数是可行的(如果有可行函数的话)。候选集合中的每个模板实例都是可行的,因为模板实参推断保证函数可以被调用。
(3):如果需要转换来进行调用,根据转换的种类排列可靠函数,记住,调用模板函数实例所允许的转换是有限的。
a:如果只有一个函数可选,就调用这个函数。
b:如果调用有二义性,从可行函数集合中去掉所有函数模板实例。
(4):重新排列去掉函数模板实例的可行函数。如果只有一个函数可选,就调用这个函数。否则,调用有二义性。