学习C++模板元编程(7)

 
到目前为止,我们还没有实现一个完整的编译期二分树,不过马上就会有了。紧接着上一篇的那道习题,是一道要求实现二分查找算法的题目,题目要求写出一个算法元函数 binary_tree_search,在一个由binary_tree_inserter生成的tree<>中查找给定的元素。如下:
typedef mpl::copy<
          mpl::vector_c
        , binary_tree_inserter< tree<> >
        >::type bst;
 
    typedef binary_tree_search >::type pos1;
    typedef binary_tree_search >::type pos2;
    typedef mpl::end::type                     end_pos;
 
    BOOST_STATIC_ASSERT((!boost::is_same< pos1,end_pos >::value));
    BOOST_STATIC_ASSERT((boost::is_same< pos2,end_pos >::value));
 
可以看到,这段代码中已经需要对 tree<>实现end<>了,binary_tree_search和end都是以tree<>的迭代器为返回结果的,所以我们需要实现的是一个比较完整的带有迭代器的二分树。参照mpl::vector<>等容器的实现,这个二分树(后面我将使用btree<>这个名字,我觉得它比tree<>要准确一些)及其迭代器(我将它命名为btree_iter<>)应该可以与mpl::begin<>、mpl::end<>、mpl::deref<>、mpl::next<>、mpl::prior<>等结合使用。我的想法是,先在btree<>的namespace里提供所需的元函数,然后在boost::mpl namespace里特化这五个元函数,特化时只需要简单地来个metafunction forwarding就行了。
typedef mpl::copy<
          mpl::vector_c
        , binary_tree_inserter< tree<> >
        >::type bst;
 
    typedef binary_tree_search >::type pos1;
    typedef binary_tree_search >::type pos2;
    typedef mpl::end::type                     end_pos;
 
    BOOST_STATIC_ASSERT((!boost::is_same< pos1,end_pos >::value));
    BOOST_STATIC_ASSERT((boost::is_same< pos2,end_pos >::value));
 


下面我们就来逐个实现这些元函数。首先是二分树、迭代器和几个辅助类的定义:
struct none {};
struct left_c {};
struct right_c {};
struct btree_tag {};
 
template 
struct btree
{
    typedef btree_tag tag;
    typedef btree type;
    typedef R root;
    typedef LC left;
    typedef RC right;
};
 
template , class Pos = mpl::vector<> >
struct btree_iter
{
    typedef mpl::bidirectional_iterator_tag category;
    typedef btree_iter type;
    typedef T btree;
    typedef Pos position;
};
 
none在前面已经出现过了, left_c和right_c分别表示左子树和右子树,它们将用于二分树迭代器中,btree_tag是按照MPL的惯例给二分树起的一个tag。btree是抄的前面的tree,只不过名字改了,还加了一个tag。btree_iter是二分树的迭代器,按书中的介绍,MP容器的迭代器应该包含两部分信息:容器本身和迭代器所指位置,这正是btree_iter的两个模板参数的意义。在btree_iter的定义中, category 和type是MPL的惯例,btree和position则用于从btree_iter中取出相关容器和位置信息。
btree_iter两个模板参数的缺省值可以看出,我是用mpl::vector<>来存储迭代器的位置信息的。我的想法是,在mpl::vector<>中保存从btree的根到迭代器所指位置的路径信息,在mpl::vector<>中的元素只能是left_c或right_c,分别表示路径上的每一步走的是左子树还是右子树。例如前面例子中的bst表示的二分树是这样的:
       int_<17>
       /      /
    int_<10> int_<25>
    /    /
 int_<2> int_<11>
那么指向元素 int_<11>的迭代器应该是:
btree_iter< bst, mpl::vector< left_c, right_c > >
即从 bst的根开始,先走一步左子树,再走一步右子树,就到达元素int_<11>了。这种迭代器的表示法对于树中的结点都可以唯一标识,但是如何表示二分树的end结点呢?按照惯例,容器是采用前闭后开区间方式的,end结点并不是树中的有效结点。我的办法是,在树中最后一个结点(当然是按中序来算了)的基础上,在其mpl::vector<>后加一个right_c来表示该结点的下一个结点,即是end了。所以,对于上面的bst例子,有:
mpl::end< bst > == btree_iter< bst, mpl::vector< right_c, right_c > >
即从 bst的根开始,走一步右子树就已经到达树的最后一个结点,再加一个right_c就表示end了。
根据上述说明,我们就可以得出 btree的begin和end元函数了,如下:
template 
struct btree_begin
   : btree_iter >
{
};
 
template 
struct btree_begin< btree >
    : btree_iter<
        btree,
        typename mpl::push_front<
            typename btree_begin::type::position,
            left_c
        >::type
    >
{
};
 
template 
struct btree_end
    : mpl::eval_if<
        typename boost::is_same::type,
        btree_iter >,
        btree_iter >
    >
{
};
 
template 
struct btree_end< btree >
    : btree_iter<
        btree,
        typename mpl::push_front<
            typename btree_end::type::position,
            right_c
        >::type
    >
{
};
 
简单说明一下, btree_begin的方法是,从根起,取左子树的btree_begin,然后前面加一步left_c(用mpl::push_front<>算法),循环的结束条件是直达单个元素,这时返回mpl::vector<>。同样,btree_end的方法是,从根起,取右子树的btree_end,然后前面加一步right_c,循环的结束条件是直达单个元素,这时候视乎该元素是否none来决定返回mpl::vector<>或者mpl::vector
在进入到 deref/next/prior之前,我们需要准备很多辅助元函数,首先是上一篇曾经出现过的root/left_child/right_child,我在这里重复一下:
template 
struct root
{
    typedef T type;
};
 
template 
struct root< btree >
{
    typedef R type;
};
 
template 
struct left_child
{
    typedef none type;
};
 
template 
struct left_child< btree >
{
    typedef LC type;
};
 
template 
struct right_child
{
    typedef none type;
};
 
template 
struct right_child< btree >
{
    typedef RC type;
};
 
接着是在此基础之上实现的 has_left/has_right,分别用于判断传入的模板参数所代表的二分树是否有左/右子树。代码如下:
template  // has_left iff left_child isn't "none"
struct has_left
    : mpl::not_<
        typename boost::is_same< 
            none, 
            typename left_child::type
        >::type
    >
{
};
 
template  // has_right iff right_child isn't "none"
struct has_right
    : mpl::not_<
        typename boost::is_same< 
            none, 
            typename right_child::type
        >::type
    >
{
};
 
接下来是 sub_btree,它接受两个模板参数,一个是btree,另一个是以mpl::vector<>表示的position,它返回二分树中给定位置以下的整棵子树。它的代码有点复杂:
template 
struct sub_btree;
 
template  // Pos is not empty
struct sub_btree2
    : mpl::eval_if<
        typename boost::is_same<
            typename mpl::front::type, left_c
        >::type,
        sub_btree<
            typename left_child::type, 
            typename mpl::pop_front::type
        >,
        sub_btree<
            typename right_child::type, 
            typename mpl::pop_front::type
        >
    >
{
};
 
template 
struct sub_btree
    : mpl::eval_if<
        typename mpl::empty::type,
        mpl::identity,
        sub_btree2
    >
{
};
 
为了方便, sub_btree由两个元函数来实现,第一个sub_btree先判断传入的Pos参数是否为空的mpl::vector<>,如果是则返回整棵二分树,否则调用第二个元函数sub_btree2。sub_btree2则根据非空的mpl::vector<>的第一个元素来判断取二分树的左子树或右子树,再去掉mpl::vector<>的第一个元素后,递归调用sub_btree来取得结果。递归的结束条件是mpl::vector<>为空。
有了 sub_btree,再结合root,我们就可以很容易实现mpl::deref了。为了清楚起见,最后再一并列出mpl::deref的代码。
接着往下,为了实现 next和prior,还需要几个辅助元函数。先看看left_trim/right_trim,它们分别用于从给定的序列(也即容器)中去掉最开头/最未尾(即最左/最右)的、连续的给定元素。先看left_trim的代码:
template 
struct left_trim;
 
template  // S is not empty
struct left_trim2
    : mpl::eval_if<
        typename boost::is_same<
            typename mpl::front::type, T
        >::type,
        left_trim::type, T>,
        mpl::identity
    >
{
};
 
template 
struct left_trim
    : mpl::eval_if<
        typename mpl::empty::type,
        mpl::identity,
        left_trim2
    >
{
};
 
left_trim先判断序列 S是否为空,是则直接返回S,否则调用left_trim2元函数。left_trim2比较S中的第一个元素与T是否相同,是则去掉它并递归调用left_trim;否则返回S。递归的结束条件是S为空或S的首元素不为T。
right_trim的实现与 left_trim基本相同,不过由于mpl没有提供取出序列中最后一个元素的元函数,所以在right_trim2中要用mpl::prior和mpl::end来得到S的最后一个元素,代码如下:
template 
struct right_trim;
 
template  // S is not empty
struct right_trim2
{
    typedef typename mpl::prior::type>::type last;
    typedef typename mpl::eval_if<
            typename boost::is_same<
                typename mpl::deref::type, T
            >::type,
            right_trim::type, T>,
            mpl::identity
        >::type type;
};
 
template 
struct right_trim
    : mpl::eval_if<
        typename mpl::empty::type,
        mpl::identity,
        right_trim2
    >
{
};
 
以上两个元函数是为了处理 btree_iter中的mpl::vector<>序列而准备的,在实现next和prior时,我们要删掉btree_iter的mpl::vector<>序列中最后的连续重复元素,将会用到以下两个辅助元函数:
template 
struct remove_last_left_cs
    : right_trim
{
};
 
template 
struct remove_last_right_cs
    : right_trim
{
};
 
顾名思义,它们是用来删掉序列 S中最后的连续left_c和right_c元素的。
现在终于轮到 next和prior了。我们先来回顾一下二分树中按中序遍历时的下一结点与前一结点的算法(翻开任何一本数据结构的书,都可以找到)。以找下一结点为例,先判断当前结点有否右子树,若有则右子树的最小结点(即right_child的begin)即为下一结点;若无则从当前结点向父结点上溯至不是父结点的右子树为止(即remove_last_right_cs),此时父结点即为下一结点;特殊情况是,如果一直上溯到根结点也不满足“非右子树”这一条件的话,则说明已经到了最后一个结点了,其下一结点就应该是end了。根据以上算法说明,有如下代码:
template  //  has right child
struct biter_next_pos2
{
    typedef typename sub_btree::type::right sub;
    typedef typename btree_begin::type::position sub_begin;
    typedef typename mpl::push_back::type p1;
    typedef typename mpl::insert_range<
            p1,
            typename mpl::end::type,
            sub_begin
        >::type type;
};
 
template  //  has not right child
struct biter_next_pos3
{
    typedef typename remove_last_right_cs::type p1;
    typedef typename mpl::eval_if<
            typename mpl::empty::type,
            typename btree_end::type::position,
            mpl::pop_back
        >::type type;
};
 
template 
struct biter_next_pos
    : mpl::eval_if<
        typename has_right::type>::type,
        biter_next_pos2,
        biter_next_pos3
    >
{
};
 
要注意的是, biter_next_pos返回的不是一个btree_iter,而是btree_iter中的position部分。我们会在后面的mpl::next实现中再用这一返回结果生成一个btree_iter。
biter_next_pos元函数先判断当前结点有否右子树,有则调用 biter_next_pos2元函数,无则调用biter_next_pos3元函数。biter_next_pos2元函数取出右子树,再调用btree_begin得到其begin结点的position,最后将当前结点的position加上一个right_c(即右子树)再加上右子树的being之position,就得到结果了。biter_next_pos3元函数则调用remove_last_right_cs来删掉当前结点的position中最后的所有right_c元素(相当于上溯至满足“非右子树”条件的父结点为止),再判断所剩序列是否为空(即是否已上溯到根结点),来决定返回end还是返回满足条件的父结点。
prior的实现十分类似,大家结合数据结构书上的算法来读以下代码就应该清楚了,这里不再罗嗦。
template  //  has left child
struct biter_prior_pos2
{
    typedef typename sub_btree::type::right sub;
    typedef typename btree_end::type::position sub_end;
    typedef typename mpl::push_back::type p1;
    typedef typename mpl::insert_range<
            p1,
            typename mpl::end::type,
            sub_end
        >::type p2;
    typedef typename mpl::pop_back::type type;
};
 
template  //  has not left child
struct biter_prior_pos3        // Return end if Pos==begin
{
    typedef typename remove_last_left_cs::type p1;
    typedef typename mpl::eval_if<
            typename mpl::empty::type,
            typename btree_end::type::position,
            mpl::pop_back
        >::type type;
};
 
template 
struct biter_prior_pos
    : mpl::eval_if<
        typename has_left::type>::type,
        biter_prior_pos2,
        biter_prior_pos3
    >
{
};
 
有一点要说明的就是,对于二分树的第一个结点(即 begin结点)调用prior应该是什么结果呢?我的做法是返回end,表示已超出范围,而且可以达到一种类似于循环序列的效果,不过只是单向的循环,即从任一结点往前走(反复调用prior)可以回到该结点,反过来则不行。
现在我们已经准备好了所有要的东西了,剩下的就是在 boost::mpl namespace中进行特化的工作了,这相对比较简单:
namespace boost { namespace mpl {
    template 
    struct deref >
        : root::type>
    {
    };
 
    template 
    struct next >
        : btree_iter::type >
    {
    };
 
    template 
    struct prior >
        : btree_iter::type >
    {
    };
 
    template 
    struct begin >
        : btree_begin >
    {
    };
 
    template 
    struct end >
        : btree_end >
    {
    };
 
}}
 
这五个元函数都是简单地通过 metafunction forwarding来调用我们前面准备好的元函数就可以了。
现在该回到最初的题目了, binary_tree_search的算法本身是简单的:从根结点开始,如果结点值与给定值相等则找到,如果给定值小于结点值则进入左子树查找,否则进入右子树查找,重复以上过程直至找到给定值(返回与结点相对应的迭代器)或到达叶结点(即无子树可找了)返回查找失败(返回end)。
算法说起来简单,不过动手一写,发现要用 MP来实现还真有点麻烦,虽然前面已经准备好了一堆辅助元函数,不过还是花了我不少时间才写出以下代码:
template 
struct binary_tree_search;
 
template  // BT != T
struct binary_tree_search2
    : mpl::vector<>
{
    typedef mpl::false_ found;
};
 
template  // R != T
struct binary_tree_search2, T>
{
    typedef typename mpl::less::type search_left;
    typedef typename mpl::if_::type sub;
    typedef typename mpl::if_::type c;
    typedef typename binary_tree_search::found found;
    typedef typename mpl::push_front<
            typename binary_tree_search::position,
            c
        >::type type;
};
 
template 
struct binary_tree_search
{
    typedef typename mpl::equal_to<
            typename root::type,
            T
        >::type at_root;
    typedef typename mpl::eval_if<
            at_root,
            mpl::vector<>, // if root equal to T
            binary_tree_search2 // if not equal
        >::type position;
    typedef typename mpl::or_<
            at_root,
            typename binary_tree_search2::found
        >::type found;
    typedef typename mpl::eval_if<
            found,
            btree_iter,
            btree_end
        >::type type;
};
 
binary_tree_search有四个 typedef,at_root用于判断BT的根结点是否与给定值T相同,position表示找到给定值的结点位置(即btree_iter内的position部分),found表示能否找到给定值,type保存返回用的结果。binary_tree_search2内的typedef则只需要关注found和type,found的意义与binary_tree_search的相同,type则相当于binary_tree_search的position。
binary_tree_search2分为两个版本,主模板版本表示已经找到叶结点,因此将 found置为mpl::false_;另一个偏特化版本则表示未到叶结点,但当前结点值R与给定值T不等,所以要判断R与T的大小以决定进一步查找左子树还是右子树,并同时在type中记下这一步的方向(left_c或right_c)。
binary_tree_search只有一个版本,它先比较根结点与给定值 T,相等则表示找到,置position为mpl::vector<>,否则调用binary_tree_search2来查找;最终根据found值来决定返回btree_iter或btree_end
binary_tree_search与 binary_tree_search2相互递归调用,递归结束条件为到叶结点为止。由于我在这段代码中没有充分利用缓式评估(lazy evaluation)和短路行为(short-circuit behavior),递归不会在查找成功时结束,它还是会一直持续到叶结点为止。
最后是测试用代码与执行结果:
int main()
{
    typedef mpl::copy<
          mpl::vector_c
        , binary_tree_inserter< btree<> >
        >::type bst;
 
    typedef binary_tree_search >::type pos1;
    typedef binary_tree_search >::type pos2;
    typedef mpl::end::type                     end_pos;
 
    std::cout << boost::is_same< pos1,end_pos >::value << std::endl;
    std::cout << mpl::deref::type::value << std::endl;
    std::cout << boost::is_same< pos2,end_pos >::value << std::endl;
 
    return 0;
}
 
Output:
0
11
1
 
这道习题确实花费了我不少的时间,花了几天的时间,反复修改和调试才最后完成。通过这次练习,终于可以比较熟练地使用 MPL中的容器和算法,也开始摸到了一些MP的门路。
 
 

你可能感兴趣的:(c++,编程,class,struct,vector,tree)