Essential C++ Chapter 6学习记录

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需要的内存空间。否则就调用BTnodeinsert_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
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()BinaryTreeclass template的一个私有成员函数,为了让上述output运算符顺利调用print(),output运算符必须成为BinaryTree的一个friend。

ps:我写的时候没有friend这句也可以。

template<typename elemType>
class BinaryTree
{
	friend ostream& operator<<(ostream&,const BinaryTree<elemType>&);
	// ...
};

6.1~6.5节的代码在这里

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节的LessThanfunction 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与上面相同
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值