本章讲解高级的基础概念,包括关键字typename的另外一种使用,将成员函数和嵌套类定义为模板,模板模板参数(template template parameters),0值初始化和在类模板中使用字符串常量的一些细节等等。
1、关键字typename
关键字typename目的是向编译说明它所有修饰的标识符是一个类型而不是其它的什么东西。如下:
template <typename T>
class MyClass {
typename T::SubType * ptr;
…
};
上述例子中的第二个关键字typename的意思是,SubType是类型T的一个类型,因此ptr就是一个执行类型T::SubType的指针。
如果没有关键字typename,编译会理解为SubType是类型T的一个静态成员,那么代码”T::SubType * ptr”就会被编译成T::SubType与ptr的乘积。
通常如果某个与模板参数(tmplate parameter)相关的名称是类型的时候就必须加上关键字typename。
如下,一个典型的应用是使用STL容器的迭代器:
// print elements of an STL container
template <typename T>
void printcoll (T const& coll)
{
typename T::const_iterator pos; // iterator to iterate over coll
typename T::const_iterator end(coll.end()); // end position
for (pos=coll.begin(); pos!=end; ++pos) {
std::cout << *pos << ' ';
}
std::cout << std::endl;
在上述的例子中,模板函数的参数T是个容器,使用容器的迭代器访问容器的元素。迭代器类型是STL容器的const_iterator,STL的声明如下:
class stlcontainer {
…
typedef … iterator; // iterator for read/write access
typedef … const_iterator; // iterator for read access
…
};
因此,使用容器T的类型const_iterator的时候要加关键字typename。
.template 的使用,
如下:
template<int N>
void printBitset (std::bitset<N> const& bs)
{
std::cout << bs.template to_string<char,char_traits<char>,
allocator<char> >();
}
上述中.template看起来很奇怪,但是如果没有.template编译器就无法知道紧跟其后的“<”是模板参数列表的起始符,而不是小于号。
注:只有当“.”之前的构件取决于模板参数的时候,这个问题才会发生。如上述的例子中bs是取决于模板参数N。
结论:“.template”或者“->template”只在模板中使用,并且它们必须紧跟着与模板参数相关的构件。
2、使用this->
如果类模板有基类,那么在类中出现的成员x并不是等价于this->x,即使x是继承而来的。
template <typename T>
class Base {
public:
void exit();
};
template <typename T>
class Derived : Base<T> {
public:
void foo() {
exit(); // calls external exit() or error
}
};
上述例子,foo()函数中调用了exit()函数,虽然基类中有exit()函数但是会被编译器忽略。使用定义在基类模板中的符号时,未来避免不确定性,最好使用“this->” 或者 “Base::” 来修饰。
3、成员模板(Member Templates)
类成员也可是模板,既可以是嵌套类模板,也可以是成员函数模板。
还是以之前章节的类模板Stack<>举例,通常只有类型相同的类Stack<>才能相互赋值,而不同的类型之间不能赋值,即使两种类型之间是可以隐式转换的。
Stack<int> intStack1, intStack2; // stacks for ints
Stack<float> floatStack; // stack for floats
…
intStack1 = intStack2; // OK: stacks have same type
floatStack = intStack1; // ERROR: stacks have different types
默认赋值操作要求左右两边的类型相同。上述例子中参数是float和参数是int的Stack是两个不同的类型,因此不能使用默认赋值操作。
如果要使得不同类型的stack可以相互赋值,就要将赋值操作定义为一个模板,如下:
template <typename T>
class Stack {
private:
std::deque<T> elems; // elements
public:
void push(T const&); // push element
void pop(); // pop element
T top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
// assign stack of elements of type T2
template <typename T2>
Stack<T>& operator= (Stack<T2> const&);
};
上述例子与原来的Stack有两次不同:
1、声明了一个赋值操作,此操作使Stack被不元素数类型T2的Stack赋值。
2、使用dque作为Stack的内部容器。
赋值操作的定义:
template <typename T>
template <typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{
Stack<T2> tmp(op2); // create a copy of the assigned stack
elems.clear(); // remove existing elements
while (!tmp.empty()) { // copy all elements
elems.push_front(tmp.top());
tmp.pop();
}
return *this;
}
上述例子中,在模板参数为T的模板中定义了一个模板参数为T2的成员模板。
template <typename T>
template <typename T2>
…
有了这个成员模板,就可以把int的Stack赋值给了float的Statck,如下:
Stack<int> intStack; // stack for ints
Stack<float> floatStack; // stack for floats
…
floatStack = intStack; // OK: stacks have different types,
// but int converts to float
当然上面的赋值并没有改变Stack中的元素类,float的Stack中元素仍然是float。
上述例子可能会让人认为,这么做会让类型检查失效,但实际上是不会的。
必要的类型检查会在源Stack的元素copy到目的Stack中时进行:
elems.push_front(tmp.top());
例如,将一个string的Stack赋值到float的Stack在编译的时候就会产生错误,如下 :
Stack<std::string> stringStack; // stack of ints
Stack<float> floatStack; // stack of floats
…
floatStack = stringStack; // ERROR: std::string doesn't convert to float
注:前面定义的模板赋值操作并不会取代默认的赋值操作,对于同元素类型的Stack赋值调用的仍然是默认的赋值操作。
同样,内部容器的类型也可以参数化,如下:
template <typename T, typename CONT = std::deque<T> >
class Stack {
private:
CONT elems; // elements
public:
void push(T const&); // push element
void pop(); // pop element
T top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
// assign stack of elements of type T2
template <typename T2, typename CONT2>
Stack<T,CONT>& operator= (Stack<T2,CONT2> const&);
};
template <typename T, typename CONT>
template <typename T2, typename CONT2>
Stack<T,CONT>&
Stack<T,CONT>::operator= (Stack<T2,CONT2> const& op2)
{
Stack<T2> tmp(op2); // create a copy of the assigned stack
elems.clear(); // remove existing elements
while (!tmp.empty()) { // copy all elements
elems.push_front(tmp.top());
tmp.pop();
}
return *this;
}
对于类模板,只有被调用的成员才会被实例化。如果不使用不同类型的Stack的赋值操作,那么内部容器也可以是vector。
Stack<int,std::vector<int> > vStack;
…
vStack.push(42);
vStack.push(7);
std::cout << vStack.pop() << std::endl;
由于上面定义的vStack没有调用到赋值操作,所以不会产生错误。
4、模板模板参数(Template Template Parameters)
模板参数本身也可是是一个类模板。
为了使用其他的类型的元素类型,stack使用的时候必须两次指定元素类型:一次是元素类型本身,另外一次是荣幸的类型。如下:
Stack<int,std::vector<int> > vStack; // integer stack that uses a vector
上述,int指定了两次。如果,使用模板模板参数(template template parameters ),那么上述中vector就不需要指定了,如下:
stack<int,std::vector> vStack; // integer stack that uses a vector
为了实现上述的定义,需要将第二个目标参数声明为模板模板参数,如下:
template <typename T,
template <typename ELEM> class CONT = std::deque >
class Stack {
private:
CONT<T> elems; // elements
public:
void push(T const&); // push element
void pop(); // pop element
T top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
上述例子,与之前的差别是第二个模板参数被声明为了类模板。
template <typename ELEM> class CONT
其默认值有原来的 std::deque改为了 std::deque,这个参数必须是类模板,并使用第一个参数实例化。
CONT<T> elems;
此例子比较特殊,使用第一个模板参数实例化了第二个模板参数,实际使用的时候可以使用类模板内的任何的类型实例化模板模板参数。
由于有模板模板参数中的模板参数“ELEM”在此例中并没有被使用,因此此例中可以省略,如下:
template <typename T,
template <typename> class CONT = std::deque >
class Stack {
…
};
类模板中的成员函数也必须按照原则修改:必须将第二个模板参数改为模板模板参数,如下:
template <typename T, template <typename> class CONT>
void Stack<T,CONT>::push (T const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
注意:函数模板是不允许使用模板模板参数了。
模板模板参数的匹配
如果用上面模板模板参数的例子编译的化,会产生编译错误。这是因为模板模板参数不但要求是一个模板,还要满足其参数必须严格匹配它所替换的模板模板参数的参数。模板模板参数的默认值是不被考虑的,因此如果不给默认值参数编译就会出错。
本例子中的标准模板库中的std::deque实际有两个参数,第二个参数有默认值是个配置器(allocator),但是当它用来匹配CONT的参数时编译器会忽略这个默认值,如下:
我们可以声明CONT包含两个模板参数,如下:
template <typename T,
template <typename ELEM,
typename ALLOC = std::allocator<ELEM> >
class CONT = std::deque>
class Stack {
private:
CONT<T> elems; // elements
…
};
由于ALLOC没有使用,所有也可是省略,如下:
template <typename T,
template <typename ELEM,
typename = std::allocator<ELEM> >
class CONT = std::deque>
5、零值初始化(Zero Initialization)
对于基本的类型,如int、float、指针类型,没有默认的构造函数将它们初始化为有意义上的值。任何一个没有初始化的局部变量,其值都是未定义的。
在模板中声明一个变量并打算初始化,但是如果变量是内建类型(built-in type),就不能确保变量会被正确初始化,如下:
template <typename T>
void foo()
{
T x; // x has undefined value if T is built-in type
}
为解决这个问题,可以在定义变量的时显示调用默认构造函数,使其值为0(bool值是false),如下:
template <typename T>
void foo()
{
T x = T(); // x is zero (or false)ifT is a built-in type
}
类模板的各个成员其类型可能被参数化。为确保初始化这样的程序,必须定义一个构造函数,在程序初始化列表中对每一个成员进行初始化,如下:
template <typename T>
class MyClass {
private:
T x;
public:
MyClass() : x() { // ensures that x is initialized even for built-in types
}
…
};
6、使用字符串常量(String Literals)作为函数模板参数
以引用(by reference)的方式将字符串常量作为函数模板参数,有时会出现意想不到的错误:
#include <string>
// note: reference parameters
template <typename T>
inline T const& max (T const& a, T const& b)
{
return a < b ? b : a;
}
int main()
{
std::string s;
::max("apple","peach"); // OK: same type
::max("apple","tomato"); // ERROR: different types
::max("apple",s); // ERROR: different types
}
上述出现的问题是,由于字符串常量长度不同,底层的array的类型也是不相同的。
“apple” 和 “peach”的array类型都是char const[6],而”tomato“的array类型是char const[7].
如果是以传值(by value)的方式就可以传递不同的字符串常量,如下:
#include <string>
// note: nonreference parameters
template <typename T>
inline T max (T a, T b)
{
return a < b ? b : a;
}
int main()
{
std::string s;
::max("apple","peach"); // OK: same type
::max("apple","tomato"); // OK: decays to same type
::max("apple",s); // ERROR: different types
}
上述例子不同字符串传递可行,是因为采用传值得方式会将数组转换为指针(array-to-pointer 经常被称为退化)。
如下:
#include <typeinfo>
#include <iostream>
template <typename T>
void ref (T const& x)
{
std::cout << "x in ref(T const&): "
<< typeid(x).name() << '\n';
}
template <typename T>
void nonref (T x)
{
std::cout << "x in nonref(T): "
<< typeid(x).name() << '\n';
}
int main()
{
ref("hello");
nonref("hello");
}
上述例子中分别使用传值和传引用的方式,传递字符串常量,结果如下:
x in ref(T const&): char [6]
x in nonref(T): const char *
7、总结
1、当访问一个依赖于模板参数的类型名时,必须在前面加上关键字typename
2、嵌套类和成员函数也可以是模板。应用之一是可以实现对不同类型但可以相互隐式转换的模板类相互转换,隐式转换时会发生类型检查。