Chapter 6 以template进行编程
人们刚开始将template称为被参数化的类型(parameterized type):
**参数化:**指类型相关信息可自template定义中剥离。
**类型:**指每一个class template或function template基本上都随着它所作用或它所内含的类型而有性质上的变化,因此他们本身就像是某种类型。
后来名称被改为template(模板)。Template定义扮演的是“处方”角色,能根据用户指定的特定值或特定类型,自动产生一个函数或类。
我们之前大量使用了诸如vector,string等class template,本章,将实现一个binary tree class template。该二叉树包含两个class:
-
BinaryTree
,用以储存一个指针,指向根节点; -
BTnode
,用来储存节点实值,以及连接至左、右两个子节点的链接。而“节点实值的类型(value type)”正是我们希望加以参数化的部分。
BinaryTree
提供insert,remove,find,print等操作,并且支持三种遍历方式:中序(inorder),前序(preorder),后序(postorder)。
在具体实现中,第一个插入空树(empty tree)的值,会成为此树的根节点。接下来的每个节点都以特定规则插入:如果小于根节点,置于左子树,大于则右子树。任何一个值只能在树中出现一次,但是此树能记录同一值插入次数。
BinaryTree<string> bt;
bt.insert("Piglet"); // 根节点
bt.insert("Eeyore"); // Eeyore在字典的顺序小于Piglet,因此成了Piglet的左子节点
bt.insert("Roo"); // Roo大于Piglet
bt.insert("Trigger");
bt.insert("Chris");
bt.insert("Pooh");
bt.insert("Kanga");
最终形成的二叉树如下所示:
任何遍历算法(traversal algorithm)皆由根结点出发:
前序遍历:遍历的节点本身先被打印,然后左子树,右子树
piglet,Eeyore,Chris,Kanga,Roo,Pooh,Trigger
中序遍历:左子树,节点本身,右子树
Chris,Eeyore,Kanga,Piglet,pooh,Roo,Trigger
后序遍历:左子树,右子树,节点本身
Chris,Kanga,Eeyore,Pooh,Trigger,Roo,Piglet
6.1 被参数化的类型
对于一个non-template BTnode
class,我们可能会这样定义:
class string_BTnode{
public:
// ...
private:
string _val;
int _cnt;
string_BTnode *_lchild;
string_BTnode *_rchild;
};
由于缺乏template机制,为了储存不同类型的数值,就得实现不同的BTnode
类,并且为其取不同的名称。
template机制能将类定义中**“与类型相关(type-dependent)”和“独立于类型之外”**的两部分分离开,实现如下:
template <typename valType>
class BTnode{
// ...
private:
valType _val;
int _cnt;
BTnode *_lchild;
BTnode *_rchild;
}
与类型相关的成分会被抽取出来,成为一个或多个参数。
后者在本例中为遍历二叉树,插入节点等类似的行为,这些行为并不会随着处理的类型不同而有所不同。
6.2 Class Template的定义
template <typename Type>
class BinaryTree;
template <typename valType>
class BTnode{
friend class BinaryTree<valType>;
public:
BTnode(const valType &);
void insert_value(const valType &);
void remove_value(const valType &,BTnode *&);
void preorder(BTnode *pt,ostream &os) const;
void inorder(BTnode *pt,ostream &os) const;
void postorder(BTnode *pt,ostream &os) const;
static void lchild_leaf(BTnode *leaf,BTnode *subtree);
private:
valType _val;
int _cnt;
BTnode *_lchild;
BTnode *_rchild;
void display_val( BTnode *pt, ostream &os ) const;
};
template <typename elemType>
class BinaryTree{
friend ostream& operator<< (ostream& os,const BinaryTree<elemType> &bt);
public:
BinaryTree();
BinaryTree(const BinaryTree &);
~BinaryTree();
BinaryTree& operator=(const BinaryTree&);
void clear(){if(_root){clear(_root);_root = 0;}}
void insert(const elemType &);
void remove(const elemType &);
bool empty(){ return _root == 0; }
void inorder() const { _root->inorder( _root, cout ); }
void postorder() const { _root->postorder( _root, cout ); }
void preorder() const { _root->preorder( _root, cout ); }
ostream& print( ostream &os);
private:
// BTnode必须以template parameter list加以限定
BTnode<elemType> *_root;
// 将src所指子树复制到tar所指子树
void copy(BTnode<elemType>* tar,BTnode<elemType>*src);
void clear(BTnode<elemType>*);
void remove_root();
};
为class template定义一个inline函数,和non-template class一样,如empty()
所示,但在类体外,class template member function的定义语法如下:
template <typename elemType>
inline BinaryTree<elemType>::
BinaryTree () : _root(0)
{}
该成员函数定义始于关键字template
和一个参数列表,然后是函数定义本身,并带有inline
及class scope运算符,inline一词必须紧接在关键字template
和参数列表之后。
上面的代码出现了两次BinaryTree
,第二次出现的不需要再加限定符,这是因为在class scope运算符BinaryTree<elemType>::
出现后,其后所有东西都被视为位于class定义范围内。当我们写:
BinaryTree<elemType> :: // 在class定义范围之外
BinaryTree() // 在class定义范围之内
第二次出现的BinaryTree
便被视为class定义范围内,所以不需要再加以限定。
下面是copy constructor,copy assignment constructor及destructor的定义:
template <typename elemType>
inline
BinaryTree<elemType>::
BinaryTree(const BinaryTree &rhs)
{copy(_root,rhs._root);}
template <typename elemType>
inline BinaryTree<elemType>::
~BinaryTree()
{clear();}
// 错误写法:
// template <typename elemType>
// inline BinaryTree<elemType>::
// BinaryTree& operator=(const BinaryTree &rhs)
// {
// if(this != &rhs){
// clear();
// copy(_root,rhs._root);
// }
// return *this;
// }
template <typename elemType>
inline BinaryTree<elemType>&
BinaryTree<elemType>::
operator= (const BinaryTree &rhs)
{
if(this != &rhs){
clear();
copy(_root,rhs._root);
}
return *this;
}
6.3 Template类型参数的处理
“template类型参数”比处理“明确的类型参数”复杂一些,例如,如果要为函数声明一个明确的**int
参数**,我们会这么写:
bool find(int val);
以传值(by value)的方式进行参数的传递,如果声明**Matrix
class为函数的参数**,我们可能改用传址(by reference)方式传递:
bool find(const Matrix &val);
当然bool find(Matrix val);
也没有错,但传址可避免因Matrix
对象的复制而造成的不必要开销,
但我们处理template类型参数时,无法得知用户实际要用的类型是否为语言内置类型,因此使用传值还是传址方式来编写find()参数列表是一个问题。
在实际应用中,无论内置类型或class类型,都可能被指定为class template的实际类型。因此建议将所有的template类型参数视为“class类型”来处理,这意味着我们会把他声明为一个const
reference。
就像在6.2节中BinaryTree
的构造函数那样,BTnode
的构造函数我们依旧选择在member intialization list内为每个类型参数进行初始化操作:
template<typename valType>
inline BTnode<valType>::
BTnode(const valType &val)
: _val(val)
{
_cnt = 1;
_lchild = _rchild = 0;
}
// 由于valType可能是class类型,使用上面的定义方式可以使效率最佳,当valType是内置类型时,并无效率上的差异
// template<typename valType>
// inline BTnode<valType>::
// BTnode(const valType &val)
// {
// // not recommanded:
// _val = val;
// // ok:
// _cnt = 1;
// _lchild = _rchild = 0;
// }
效率差的原因:
constructor函数体内对_val
的赋值操作可分解为两个步骤:
(1)函数体执行前,Matrix
的default constructor会先作用于_val
身上
(2)函数体内会以copy assignment operator将val
复制给_val
当我们使用上述第一种方法时,在constructor的member initialization list中将_val
初始化,那么只需一个步骤就能完成工作:以copy constructor将val
复制给_val
6.4 实现一个Class Template
每当我们插入某个新值,都必须建立BTnode
对象、加以初始化,将他链接至二叉树的某处。我们必须自行以new
表达式和delete
表达式来管理每个节点的内存分配和释放
以insert()
为例,若根节点尚未设定,则该函数会由程序的空闲空间分配一块新的BTnode
需要的内存空间。否则就调用BTnode
的insert_value()
,将新值插入二叉树中:
template <typename elemType>
inline void
BinaryTree<elemType>::
insert(const elemType &elem)
{
if(! _root)
_root = new BTnode<elemType> (elem);
else
_root -> insert_value(elem);
}
new
表达式可分解为两个操作:
(1)向程序的空闲空间请求内存。如果分配到足够空间,就返回一个指针,指向新对象(若空间不足,会报出bad_alloc异常)
(2)若第一步成功,并且外界指定了一个初值,这个新对象便会以最适当的方式被初始化,对class类型来说:
_root = new BTnode<elemType> (elem);
elem会被传入BTnode
constructor。如果分配失败,初始化操作就不会发生。
当根节点存在时,insert_value()
才会被调用,小于根节点的所有数值都放在根节点的左子树,大于则在右子树。该函数会通过左右子节点递归(recursively)调用自己,知道以下任何一种情形发生才停止:
(1)合乎资格的子树并不存在
(2)欲插入的数值已在树中
template <typename valType>
void inline BTnode<valType>::
insert_value(const valType &val)
{
if(val == _val)
{
_cnt++;
return;
}
if(val < _val)
{
if(! _lchild)
_lchild = new BTnode(val);
else
_lchild -> insert_value(val);
}
if(val > _val)
{
if(! _rchild)
_rchild = new BTnode(val);
else
_rchild -> insert_value(val);
}
}
移除操作:我们必须保持二叉树的次序不变。一般的算法为:以节点的右子节点取代节点本身,然后搬移左子节点,使它成为右子节点的左子树的叶结点。如果此刻并无右子节点,那么就以左子节点取代节点本身。为了简化,我们将根结点的移除操作以特例处理:
![image-20220115125111916](第六章.assets/image-20220115125111916.png)
template<typename elemType>
inline void BinaryTree<elemType>::
remove(const elemType &elem)
{
if(_root)
{
if(_root->_val == elem)
remove_root();
else
_root->remove_value(elem,_root);
}
}
无论是remove_root()
或remove_value()
,都会搬移左子节点,使他成为右子节点的左子树的叶子节点。我们将这一操作剥离至lchild_leaf()
,那是BTnode
的static member function:
template<typename valType>
inline void BTnode<valType>::
lchild_leaf(BTnode *leaf,BTnode *subtree)
{
while(subtree->_lchild)
subtree = subtree->_lchild;
subtree->_lchild = leaf;
}
以下讨论根节点的移除:
如果根节点拥有任何子节点,remove_root()
就会重设根节点,如果右子节点存在,就以右子节点取而代之;如果左子节点存在,就直接搬移,或通过lchild_leaf()
完成。如果右子节点为null,_root
便以左子节点取代。
template<typename elemType>
inline void BinaryTree<elemType>::
remove_root()
{
if(! _root) return;
BTnode <elemType> *tmp = _root;
if(_root->_rchild)
{
_root = _root -> _rchild;
if(tmp -> _lchild)
{
BTnode<elemType> *lc = tmp -> _lchild;
BTnode<elemType> *newlc = _root -> _lchild;
if(!newlc)
_root -> _lchild = lc;
else
BTnode<elemType>::lchild_leaf(lc,newlc);
}
}
else
_root = _root->_lchild;
delete tmp;
}
remove_value()
有两个参数:将被删除的值(如果存在的话)以及一个指针,指向目前关注的节点的父节点。
第二个参数,我们将prev
以一个reference to pointer来传递,以pointer传递只能更改该pointer所指之物,而不是pointer本身。为了改变pointer本身,必须再加一层间接性。如果将prev
声明为reference to pointer,不但可以改变pointer本身,也能改变由此pointer指向的对象。
template<typename valType>
inline void BTnode<valType>::
remove_value(const valType &val,BTnode *& prev)
{
if(val < _val)
{
if(! _lchild)
return;
else
_lchild -> remove_value(val,_lchild);
}
else if(val > _val)
{
if(!_rchild)
return;
else
_rchild -> remove_value(val,_rchild);
}
else
{
if(_rchild)
{
prev = _rchild;
if( _lchild)
if(! prev -> _lchild)
prev -> _lchild = _lchild;//改变pointer指向的对象
else
BTnode<valType>::lchild_leaf(_lchild,prev->_lchild);
}
else
prev = _lchild;// 改变pointer本身
delete this;
}
}
clear()
函数用来移除整棵二叉树,我们把该函数分为两份:一个是inline public函数,另一个是前者的重载版本,用以执行实际工作,并放在private部分。
template <typename elemType>
class BinaryTree{
public:
// ...
void clear(){if(_root){clear(_root);_root = 0;}}
private:
// ...
void clear(BTnode<elemType>*);
};
template <typename elemType>
void BinaryTree<elemType>::
clear(BTnode<elemType> *pt)
{
if(pt){
clear(pt->_lchild);
clear(pt->_rchild);
delete pt;
}
}
6.5 一个以Function Template完成的Output运算符
针对non-template class提供output运算符时,写法一般如:
ostream& operator<<(ostream&,const int_BinaryTree&);
若对象是class template,可以这样写:
ostream& operator<<(ostream&,const BinaryTree<int>&);
但更好的解法是
template<typename elemType>
inline ostream&
// 书里代码的第二个参数是用const修饰的,但我使用的编译器会认为print()函数
// 可能会改变bt的值,与const冲突,因此会报错。
// operator<< (ostream&,const BinaryTree<elemType> &bt)
operator<< (ostream& os,BinaryTree<elemType> &bt)
{
os<<"Tree: "<<endl;
bt.print(os);
return os;
}
这样当我们写:
BinaryTree<string> bts;
cout<<bts<<endl;
编译器就将前述第二参数指定为BinaryTree<string>
,产生一个对应的output运算符。类似的,当写:
BinaryTree<int> bts;
cout<<bts<<endl;
编译器就将前述第二参数指定为BinaryTree<int>
,又产生一个对应的output运算符。
print()
是BinaryTree
class template的一个私有成员函数,为了让上述output运算符顺利调用print()
,output运算符必须成为BinaryTree
的一个friend。
ps:我写的时候没有friend
这句也可以。
template<typename elemType>
class BinaryTree
{
friend ostream& operator<<(ostream&,const BinaryTree<elemType>&);
// ...
};
6.6 常量表达式与默认参数值
Template参数并不是非得某种类型不可,我们也可以使用常量表达式作为template参数。例如先前的数列类继承体系可以重新以class template设计,将“对象所含的元素个数”参数化。
”以表达式作为template参数“也被称为“非类型参数”
template<int len>
class num_sequence{
public:
num_sequence(int beg_pos = 1);
// ...
}
// 如何实现派生类?为了标示这个新类是继承自一个已存在的类,其名称之后必须接一个冒号,然后紧跟着关键字
// public和基类的名称
template<int len>
class Fibonacci:public num_sequence<len>{
public:
Fibonacci(int beg_pos=1)
:num_sequence<len>(beg_pos){}
}
当我们产生Fibonacci
对象
Fibonacci<16> fib1;
Fibonacci<16> fib2(17);
两个对象皆属于Fibonacci
,而其基类num_sequence会因为参数len而导致元素个数为16。
我们也可以将长度和起始位置一并参数化:
ps:这样定义的话,第五章num_sequence
class就不需要储存“长度”和“起始位置”这两个data member了
template <int len,int beg_pos>
class num_sequence{...}
template <int len,int beg_pos=1>
class Fibonacci:public num_sequence<len,beg_pos>{...}
这种类的定义方式如下:
//初始化为
//num_sequence<32,1> *pns1to32 = new Fibonacci<32,1>
num_sequence<32> *pns1to32 = new Fibonacci<32>;
// 会将默认的表达式参数值覆盖掉
num_sequence<32,33> *pns33to64 = new Fibonacci<32,33>;
这里的参数默认值和一般的默认参数值一样,由左至右进行解析。
全局作用域内的函数及对象,其地址也是一种常量表达式,因此也可以被拿来表达这一形式的参数。
template <void (*pf) (int pos,vector<int> &seq)>
class numeric_sequence
{
public:
numeric_sequence(int len,int beg_pos =1)
// ...
};
本例中的pf
是一个指向“依据特定数列类型,产生pos个元素,放到vector seq
内”的函数,用法如下:
void fibonacci (int pos, vector<int> &Seq);
void pell (int pos, vector<int> &Seq);
// ...
numeric_sequence<fibonacci> ns_fib(12);
numeric_sequence<pell> ns_pell(18,8);
6.7 以Template参数作为一种设计策略
4.9节的LessThan
function object定义和用法如下:
class LessThan
{
public:
LessThan (int val):_val(val) {}
int comp_val() const {return _val;}
void comp_val(int nval) {_val = nval;}
bool operator() (int _value) const;
private:
int _val;
};
// 其中的function call运算符实现如下:
inline bool LessThan::
operator() (int value) const {return value < _val;}
// 用法1:将LessThan call 运算符应用于对象身上,便可以调用function call运算符:
int count_less_than(const vector<int> &vec,int comp)
{
// 应用于对象身上
LessThan lt(comp);
int count = 0;
for(int ix=0; ix<vec.size(); ++ix)
// 调用function call运算符
if(lt(vec[ix]))
++count;
return count;
}
//用法2:当作参数传给泛型算法
void print_less_than(const vector<int> &vec,int comp, ostream &os = cout)
{
LessThan lt(comp);
vector<int>::const_iterator iter = vec.begin();
vector<int>::const_iterator it_end = vec.end();
os <<"elements less than "<<lt.comp_val() <<endl;
while((iter=find_if(iter,it_end,lt))!=it_end)
{
os<<*iter<<' ';
++iter;
}
}
// Ps:find_if的行为如下:
template<class InputIterator, class UnaryPredicate>
InputIterator find_if (InputIterator first, InputIterator last, UnaryPredicate pred)
{
while (first!=last)
if (pred(*first)) return first;
++first;
}
return last;
}
我们也可以将其转为class tempalte:
template <typename elemType>
class LessThan{
public:
LessThan(const elemType &val): _val(val) {}
bool operator() (const elemType &val) const
{return val < _val;} //问题出在这里,使用了less-than运算符
void val(const elemType &newval) {val = newval;}
elemType val() const {return _val;}
private:
elemType _val;
};
LessThan<int> lti(1024);
LessThan<string> lts("pooh");
上面的代码存在问题:当用户所提供的类型并未定义less-than
运算符,上面的return val < _val;
便告失败。
一种解决办法是提供第二个class template,将comparison运算符从类定义中剥离,需要注意的是,虽然第·二个类提供的是和LessThan相同的语义,我们却得为他另外取个名称,因为class template无法给予参数列表的不同而重载,我们将其命名为LessThanPred
,因为less-than运算符被我们指定为默认参数值:
template<typename elemType,typename Comp = less<elemType> >
class LessThanPred{
public:
LessThanPred(const elemType &val) : _val(val) {}
bool operator() (const elemType &val) const
{return Comp(val,_val);}
void val(const elemType &newval) {_val = newval;}
elemType val() const {return _val;}
private:
elemType _val;
};
// 另一个提供比较功能的function object
class StringLen{
public:
bool operator() (const string &s1,const string &s2)
{return s1.size() < s2.size();}
};
LessThanPred<int> ltpi(1024); // 因为是
LessThanPred<string,StringLen> ltps("pooh");
我们也可以以另一个更通用的名称来命名function object,表示它足以指出任何类型的比较操作。那么本例就不需要再提供默认的function object了:
template<typename elemType,typename BinaryComp>
class Compare;
Compare
可将任何一种“BinaryComp
操作”应用于两个同为elemType
类型的对象身上。
在第五章,我们用继承和多态实现了一个面向对象的数列体系,下面是另一种设计,它将数列类定义为class template,而将实际的数列类剥离成为参数。
template <typename num_seq>
class NumericSequence{
public:
NumericSequence(int len = 1,int bpos = 1)
: _ns(len,bpos) {}
// 以下会通过函数的命名规范,调用未知的数列类中的同名函数
// 函数命名规范是指:每个num_seq参数类都必须提供
// 名为calc_elems()和is_elem()的函数
void calc_elems(int sz) const {_ns.calc_elems(sz);}
bool is_elem(int elem) const {return _ns.is_elem(elem);}
// ...
private:
num_seq _ns;
};
以上template设计,将某种特定的命名规范强加于被当作参数的类身上:每个类都必须提供NumericSequence
class template中调用到的函数,如calc_elems()
,is_elem()
,等等。
这个例子说明了class template的类型参数不是只能用以传递元素类型(像二叉树或标准库的vector、list那样),还可以是一个类。
6.8 Member Template Function
我们也可以将memeber function定义成template形式
class PrintIt{
public:
PrintIt(ostream &os)
: _os(os) {}
// 下面是一个member template function
template <typename elemType>
void print(const elemType &elem,char delimiter = '\n')
{_os << elem << delimiter;}
private:
ostream& _os;
};
PrintIt
是一个non-template class,其初值为一个output stream(输出数据流)。它提供的print()
能够将任意类型的对象写至指定的output stream。上面的代码在“只写一份函数定义“的情况下,支持任何类型。
当然,前提是该类型可应用output运算符,如果不将”要输出的元素“的类型剥离为参数,我们就得为不同的类型格子间里一份class/采用member template,就只需要一份PrintIt
类。
// PrintIt可能的用法:
int main()
{
PrintIt to_standard_out(cout);
to_standard_out.print("hello");
to_standard_out.print(1024);
string my_string("i am a string");
to_standard_out.print(my_string);
}
// output:
hello
1024
i am a string
Class template内也可以定义member template function。例如,我们会将PrintIt
原本指定的output ostream再予以参数化,使其成为可指定的ostream类型,并仍然令print()
成为一个member template function:
template <typename OutStream>
class PrintIt{
public:
PrintIt(OutStream &os)
:_os(os){}
template<typename elemType>
void print(const elemType &elem,char delimiter = '\n')
{_os<<elem<<delimiter;}
private:
ostream& _os;
};
// 用法:
int main()
{
PrintIt<ostream> to_standard_out(cout);
// ...
}
// output与上面相同