Stroustrup拟好C++语言中关于template的原始设计后,将template称为被参数化的类型(parameterized type):称其参数化是因为,类型相关信息可自tempplate定义中剥离,称其类型则是因为,每一个class template或function template基本上随着它所作用或它所内含的类型而有性质上的变化。
Template定义扮演的乃是“处方”角色,能根据用户指定的特定值或特定类型,自动产生一个函数或类。
本章的任务之一,带你一览二叉树(binary tree) class template的所有实现过程。
所谓树(tree)乃是由节点(node)以及连接不同节点的链接(link)组成。所谓二叉树,维护着每个节点与下层另两个节点间的两条链接,一般将此下层二节点称为左子节点(left child)和右子节点(right child)。最上层第一个节点称为根节点(root)。无论是左子节点或右子节点,都可能扮演另一个'子树“的根节点。一个节点如果不在有任何子节点,便称为叶节点(leaf)。
我们的二叉树包含两个类,一个是BinaryTree,用来存放指针,指向根节点,另一个是BTnode,用来存放节点实值,以及连接至左、右两个子节点的链接。
6.1 被参数化的类型
以下是一个non-template BTnodes class,其实值存放在string对象之中。
class string_BTnode
{
public:
//...
private:
string _val;
int _cnt;
string_BTnodes* _lchild;
string_BTnodes* _rchild;
};
template机制帮助我们将类定义中”与类型相关(type-dependent)“和"独立于类型之外"的两部分分离开。
在一个class template中,与类型相关的部分会被抽取出来,成为一个或多个参数。以BTnodes class为例,data member _val的类型可被参数化。
template <typename valType>
class BTnode;
{
public:
//...
private:
valType _val;
int _cnt;
BTnode* _lchild;
BTnode* _rchild;
};
在此class template定义中,valType被拿来做一个占位符。其名称可以任意设定。在用户为它指定某个特定类型之前,它被视为可取代为任意类型的东西。
在我的实现中,BTnode class template必须和BinaryTree class template配合使用。针对每一种BTnode的实际类型,我们希望对于的BinaryTree实例能够成为其friend。声明方法如下:
template <typename valType>
class BinaryTree;
template <typename valType>
class BTnode;
{
friend class BinaryTree<valType>;
};
为了通过class template实例化类,我们必须在class template名称之后,紧接一个尖括号,其内放一个实际类型(准备用来取代valType)。
BinaryTree仅声明一笔数据:一个BTnode指针,指向二叉树根节点。
template <typename elemType>
class BinaryTree
{
public:
// ...
private:
BTnode<elemType>* _root;
};
6.2 Class Template的定义
以下便是我们的BinaryTree class template的部分定义:
template <typename elemType>
class BinaryTree
{
public:
BinaryTree();
BinaryTree(const BinaryTree&);
~BinaryTree();
BinaryTree& operator=(const BinaryTree&);
bool empty() { return _root == 0;}
void clear();
private:
BTnode<elemType>* root;
void copy(BTnode<elemType>* tar, BTnode<elemType>* src);
};
为class template定义为一个inline函数,做法与non-template class定义一个inline函数一样。
template <typename elemType>
inline BinaryTree<elemType>::
BinaryTree() : _root(0)
{}
这个member function的定义始于关键字template和一个参数列表,然后便是函数定义本身,并带有inline及class scope运算符。inline一词必须紧接着template和参数列表之后。
在class scope运算符出现之后BinaryTree<elemType>::其后所有都被视为位于class定义范围内。
以下是BinaryTree的copy constructor、copy assignment operator及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<elemType>::
operator=(const BinaryTree& rhs)
{
if (this != &rhs)
{
clear();
copy(_root, rhs._root);
}
return *this;
}
6.3 Template类型参数的处理
实际运用中,不论内置类型或class类型,都可能被指定为class template的实际类型。我建议,将所有的template类型参数视为”class类型“处理。这意味着我们会把它声明为const reference,而不是by value方式传递。
在constructor定义中,我选择在member initialization list内为每个类型参数进行初始化操作:
template <typename valType>
inline BTnode<valType>::
BTnode(const valType& val) : _val(val)
{
_cnt = 1;
_lchild = _rchild = 0;
}
这样一来,当用户为valType指定一个class类型时,可以保证效率最佳。
如果是在constructor函数体内初始化,那么初始化操作分为两步:(1)函数体执行时,default constructor会先作用于_val身上。(2)函数体内会以copy assignment operator将val复制给_val。
当如果是在member initialization list中初始化只需以copy constructor将val复制给_val。
6.4 实现一个Class Template
每当我们插入某个新值,都必须建立BTnode对象、加上初始化。
以insert()为例,如果根节点之值尚未设定。
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) 向程序的空闲空间(free store)请求内存。如果分配到足够的空间,就返回一个指针,指向新对象。(2) 如果第一步成功,并且外界指定了一个初值,这个新对象便会以最适当的方式被初始化。
当根节点存在时,insert_value()才会被调用。小于根节点的所有数值放在根节点左子树,大于根节点的所有数值放在根节点的右子树。insert_value()会通过左右子节点递归(recursively)调用自己,直到以下任何一种情形发生:(1)合乎资格的子树并不存在,(2)欲插入的数值已在树中。下面为实现内容:
template <typename valType>
void 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);
}
else
{
if (!_rchild)
_rchild = new BTnode(val);
else
_rchild->insert_value(val);
}
}
移除某值的操作更为复杂,因为我们必须保持二叉树的次序不变。一般的算法是,以节点的右子节点取代节点本身,然后搬移左子节点,使它成为右子节点的左子树的叶节点,如果此刻并无右子节点,那么就以左子节点取代节点本身。。
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()。以下是实现内容:
template <typename valType>
void BTnode<valType>::
lchild_leaf(BTnode* leaf, BTnode* subTree)
{
while (subTree->lchild)
subTree = subTree->_lchild;
subTree->lchild = leaf;
}
以下是讨论根节点
template <typename elemType>
void BianryTree<elemType>::
remove_root()
{
if (!_root)
return;
BTnode<elemType>* tmp = _root;
if (_root->_rchild)
{
_root = _root->rchild;
if (tem->_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()有两个参数:将被删除的值以及一个指针,指向目前关注的节点的父节点。第二个参数是reference to pointer。以pointer来传递,我们能够更改该pointer所指之物,而不是pointer本身。为了改变pointer本身,我们也将其声明为引用。reference to pointer,我们不但可以改变pointer,还可以改变pointer指向的对象。
template <typename valType>
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;
else
BTnode<valType>::lchild_leaf(_lchild, prev->_lchild);
}
else
prev = _child;
delete this;
}
}
6.5 一个以Function Template完成的Output运算符
template <typename elemType>
inline ostream&
operator<<(ostream& os, const BinaryTree<elemType>& bt)
{
os << "Tree: " << endl;
bt.print(os);
return os;
}
// 因为运算符访问了bt的member,因此需要成为BinaryTree的友元
template <typename elemType>
class BinaryTree
{
friend ostream& operator<<(ostream& os, const BinaryTree<elemType>& bt);
...
};
6.6 常量表达式与默认参数值
Template参数并非一定得某种类型(type)不可。我们也可以使用常量表达式(const expression)作为template参数。
template <int length>
class num_sequence
{
...
};
全局作用域(global scope)内的函数和对象,其地址也是一种常量表达式,因此也可以被拿来表达这一形式的参数。
template <void (*pf)(int, vector<int>& seq)>
class num_sequence
{
...
};
6.7 以template参数作为一种设计策略
template <typename elemType>
class LessThan
{
public:
bbol operator()(const elemType& val) const { return val < _val; }
...
};
上述做法存在一个潜在问题,一旦用户所提供的类型并未定义less-than运算符,上述便宣告失败。另一种可行策略是提供第二个class template,将conparison运算符从类定义中剥离。虽然第二个类提供的是和LessThan相同的语义,我们却得为它另外取一个名字,因为class template无法基于参数列表的不同而重载。让我们把它命名为LessThanPred吧。
template <typename elemType, typename Comp = LessThan<elemType>>
class LessThanPred
{
public:
bool operator()(const elemType& val) const { return Comp(val, _val);}
};
6.8 Member Template Function
当然,我们也可以将member function定义成template形式。
class PrintIt
{
public:
template <typename elemType>
void print(const elemType& elem, char delimiter = '\n')
{
_os << elem << delimiter;
}
private:
ostream& _os;
};
另print()成为PrintIt的member function template,我们便可以在只写一份函数定义的情形下,支持任何类型。
class template内也可以定义member function template。我们可能会将PrintIt原本指定的output stream予以参数化,使其成为可以指定的ostream类型。
template <typename OutStream>
class PrintIt
{
public:
template <typename elemType>
void print(const elemType& elem, char delimiter = '\n')
{
_os << elem << delimiter;
}
private:
ostream& _os;
};