本文继续深入探讨模板的基础知识,covers 内容如下:
typename
的另一种用法在C++标准化的过程中,引入关键字typename
是为了说明:模板类型参数内部的标识符(associated type,常见于STL中的各种容器)也可以是一个类型:
template<typename T>
class MyClass
{
typename T::SubType* ptr;
}
上述程序中,第二个typename被用来说明,SubType是定义与类T
内部的一种类型,也就是associated type
,因而,ptr是一个指向T::SubType
类型的指针。如果不使用typename
,T::SubType
会被优先看做T的一个静态成员,也就是一个具体而变量或对象,于是,下面的表达式:
T::SubType * ptr;
会被看做类T的静态成员SubType和ptr的乘积。
通常情况下,当要以类型的形式使用类型模板参数的associated types(即定义于其内部的类型)时,就必须使用typename关键字。
下面我们来考虑一个typename的典型应用场景,即在模板代码中访问STL容器的迭代器(const_iterator,作为每一个容器类的associated type)。
template<typename COLL>
void printColl(ostream& os, const COLL& coll)
{
typename COLL::const_iterator pos;
typename COLL::const_iterator end(coll.end());
for(pos = coll.begin(); pos != end; ++pos)
os << *pos << " ";
os << endl;
}
我们以一个小例子说明这一语法特性:
虽然bitset<>模板类提供了简单的与string交互(转换)的接口,如bitset<>的string参数类型的构造函数,也有十分方便的to_string
的成员函数,但两者都是成员模板,就不能像一般的成员函数那样使用。关于这部分内容更详尽的内容请见
template<size_t N>
string to_string(const bitset<N>& b)
{
return b.template
to_string<char, char_traits<char>,
allocator<char> >();
}
这个函数的目的是对b.to_string
这一成员函数模板赋值的封装,
string s = b.template to_string<char, char_traits<char>,
allocator<char> >();
// 借助于上面的辅助函数
string s = to_string<6>(b);
在老式的c++编译器中,这里的.template不能省略,如果没有这个.template,编译器无法确定,其后的<
是小于号,还是模板参数列表的起始符号。
类的成员也可以是模板。嵌套类和成员函数都可以作为模板。
我们在之前的讨论
template<typename T>
class Stack
{
public:
void push(const T& x)
{ elems.push_back(x);} // 尾端入
void pop() { elems.pop_back();} // 尾端出,实现一种FILO的机制
T& top() { return elems.back();}
const T& top() const { return elems.back();}
bool empty() const { return elems.empty();}
private:
std::deque<T> elems;
}
通常而言,栈之间只有在元素类型完全相同时才能相互赋值,换句话说,类型不同的栈,无法进行赋值,即使这两种(元素的)类型之间存在隐式类型抓换,如:
Stack<int> is1, is2;
Stack<double> ds3;
//...
is1 = is2; // OK: 具有相同类型的栈
is3 = is1; // ERROR:两边栈的类型不同
类中国缺省赋值运算符要求=
两边具有相同的元素类型。通过定义一个模板形式的赋值运算符,元素类型不同的两个栈就可实现相互赋值。
template<typename T>
class Stack
{
public:
// ...
template<typename T2>
Stack<T>& operator=(const Stack<T2>& rhs);
// 返回值不含const 属性,保证了`s1 = s2 = s3`
}
新的赋值运算符的实现大致如下:
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator=(const Stack<T2>& rhs)
{
if ((void*)this == (void*)&rhs)
return *this; // identity test,同一性测试
Stack<T2> tmp(rhs);
elems.clear();
while(!tmp.empty())
{
elems.push_back(tmp.top());
tmp.pop();
}
return *this;
}
新的类型检查发生在:
elems.push_back(tmp.top());
同样地,在实现中,也可以把内部容器类型实现为一个模板参数:
// CONT类型的容器,必须支持Stack<, >类模板体内的操作或者接口
// 如push_back, pop_back, back, empty
template<typename T, typename CONT = std::deque<T> >
class Stack
{
public:
void push(const T& x) { elems.push_back(x);}
void pop() { elems.pop_back();}
T& top() { return elems.back();}
const T& top() const { return elems.back();}
bool empty() const { return elems.empty();}
template<typename T2, typename CONT2>
Stack<T, CONT>& operator=(const Stack<T2, CONT2>& rhs);
private:
CONT elems;
}
template<typename T, typename CONT>
template<typename T2, typename CONT2>
Stack<T, CONT>& Stack<T, CONT>::operator=(const Stack<T2, CONT2>& rhs)
{
if((void*)this == (vois*)&rhs)
return *this;
Stack<T2, CONT2> tmp(rhs);
elems.clear();
while (!tmp.empty())
{
elems.push_back(tmp.top());
tmp.pop();
}
return *this;
}
这是一种口语化的说法,准确地说,以模板作为另一个模板的模板参数,是不是更绕,:-D。我们继续以Stack模板类举例,探讨模板的模板存在的必要性,再往大点说是C++模板技术演化的线索。
// 这时的第二个模板参数是一种模板的实例化
// 必须这样传递,Stack<int, deque<int> >
// Stack<int, queue<int> >
template<typename T, typename CONT = std::deque<T> >
class Stack{...};
Stack<int, std::vector<int> > intStack;
我们看到上述代码是存在冗余的(忽然想到一句话,模板类是不是可以称得上一种具有参数的类呢,正如函数一样。),即第一个模板参数是第二个模板参数的参数,且有可能发生参数的传递错误,如Stack<int, deque<double> >
。我们猜想是否存在如下的没有冗余的声明式:
Stack<int, std::vector> intStack;
为了支持这样的语法特性:
template<typename T, template<typename ELEM>
class CONT = std::deque >
class Stack
{
public:
void push(const T& x);
void pop();
T& top();
const T& top() const;
bool empty() const { return elems.empty();}
private:
CONT<T> elems;
}
这与前述代码的不同之处在于,第二个参数从一个类模板实例化变成了一个类模板,,缺省值自然也从std::deque<T>
变成了std::deque
,在使用时,第二个参数必须是一个类模板,并且由第一个模板参数传递进来的类型进行实例化(这个例子比较特殊,一般地可以在类模板内部以任意允许的类型实例化模板的模板参数):
CONT<T> elems;
之前的讲述模板基础知识的时候,我们曾说过,作为模板参数的声明,通常可以使用typename替换关键字class,然而,上述的CONT是欲定义的一个模板类,必须用关键字class修饰。
template<typename T, template<typename ELEM>
class CONT = std::deque>
class Stack { ... }; // 正确
template<typename T, template<typename ELEM>
typename CONT = std::deque>
class Stack { ... } // 错误
就像我们可以在.hpp
文件的类成员函数或者全局函数的声明中省略形参名,保留形参类型一样,我们也可以对这里的作为模板参数的模板的参数,也即ELEM
,因为不涉及的函数体,或者即是涉及函数体,在函数体内也用不到,可见在真实的内存模型中,操作的内存单元,而不是形参名(大概保存在一个叫符号表的结构中,这部分内容可参阅编译原理或者C++内存模型相关细节)。
template<typename T, template<typename>
class CONT = std::deque>
class Stack
{ ... }
另外,还必须对成员函数的成员函数的声明进行相应的修改,将第二个模板参数指定为模板的模板参数:
template<typename T, template<typename> class CONT>
void Stack<T, CONT>::push(const T& x)
{
elems.push_back(x);
}
这时如果不加修改的使用新版本的Stack
,甚至不调用客户端代码,不进行实例化,在编译阶段就将得到一个错误信息,缺省值std::deque和模板的模板参数CONT并不匹配
。问题在于,模板的模板实参(比如本例中的std::deque
)也是一个具有参数(以A为例)的模板(std::deque
的模板参数如下所示,也是一个具有两个模板参数的模板):
template<class _Ty,
class _Alloc = allocator<_Ty> >
class deque
: public
它将替换这里模板的模板参数(也即是这里的CONT),而模板的模板参数是一个具有参数B的模板(也即是本例的template)匹配过程要求参数A和参数B必须完全匹配,而这里,如前所示,并不能完全匹配,一个是两个模板参数的类模板,而一个只有一个模板参数 。这时我们可以重写类的声明,以使参数个数相匹配。
template<typename T, template<typename ELEM, template ALLOC = std::allocator<ELEM>
class CONT = std::deque>
class Stack
{
private:
CONT<T> elems; // CONT<T, allocator<T> >
// deque<T, allocator<T> >
}
同样这时,ALLOC也可省略不写,因为在实现中并不会用到。现在我们便可以写出完整版的Stack的声明与定义了:
template<typename T, template<typename ELEM, typename =
std::alloctor<ELEM> >
class CONT>
class Stack
{
public:
void push(const T&);
void pop();
T& top();
const T&() const;
bool empty() const;
// 成员模板
template<typename T2, template<typename ELEM2,
typname ALLOC=std::allocator<ELEM2> >
class CONT2>
Stack<T, CONT>& operator=(const Stack<T2, CONT2>&);
private:
CONT<T> elems; // 这里完成实例化
}
template<typename T, template<typename, typename>
class CONT>
void Stack<T, CONT>::push(const T& x)
{
elems.push_back(x);
}
///
template<typename T, template<typename, typename>
class CONT>
template<typename T2, template<typename, typename>
class CONT2>
Stack<T, CONT>& Stack<T, CONT>::operator=(const Stack<T2, CONT2>& rhs)
{
if ((void*)this == (void*)&rhs) // identity test
return *this;
Stack<T2, CONT2> tmp(rhs);
elems.clear();
while(!tmp.empty())
{
push(tmp.top());
tmp.pop();
}
return *this;
}
关于两个版本的
top()
成员函数,一个是const
的返回常量引用
的,一个是没有这些常量修饰的,这本质上上是因为Stack
模板类内部的CONT
容器的back()
函数(也即STL对所有容器类的一种要求或者说是规范,返回两个版本的返回容器内部元素,一个mutable sequence,另一个是nonmutable sequence
)
// deque,内部可变和不可变两个版本的back()成员函数
typedef typename _Mybase::reference reference;
typedef typename _Mybase::const_reference const_reference;
reference back()
{ // return last element of mutable sequence
return (*(end() - 1));
}
const_reference back() const
{ // return last element of nonmutable sequence
return (*(end() - 1));
}