问题背景:
唔,我正在写一个B+树,其中它的节点查找算法需要在不同的应用场景下采用不同的算法。我可以采用顺序查找和二分法查找两种方案,如果这个节点元素比较小时,用顺序查找的方案要快一些,当节点元素比较多时,则用二分法查找。
第一方案:用if来解决
我们会最容易想到的是
template<int nodesize>
struct CBinaryTreeNode
{
bool Search(...)
{
if(nodesize > critical_size)
{
binary_search(...); //二分法查找
}
else
{
order_search(...); //顺序查找
}
}
};
OK,这个方案很容易理解,性能也算可以,不过当我们做好第一版后,发现这个模板调用得比较多,还是一个性能瓶颈,必需进一步改善。
另外我们发现,每次都要进行一个容器大小的判断,事实上容器大小是固定的,(如B+树中的节点,它是固定的),那么我们是不是可以把这个判断去掉呢,我们一定会想到使用臭名昭著的宏定义,使用条件编译。我从来不反对使用宏,问题是在这里宏解决不了我们的问题,因为nodesize并不是固定的,而是由其它程序员使用这个类时提供的!
而且它还有一个更严重的问题,假如我们在两种查找方案中使用了不同的容器(非STL容器),比如较小的时候我们使用数组,而较大的时候我们使用了链表。一个只有通过下标运算符来取得元素内容,而另一个只提供了GetAt的方法。那么我们无法统一使用[]来取得容易中的元素,导致函数无法通过编译!!!
第二方案:理论上可行的偏特化
嗯,类模板的偏特化正好可以解决这个问题,我们可以如下定义
template<int nodesize,bool flag = (nodesize > critical_size) >
struct CBinaryTreeNode
{
bool Search(...);
};
template<int nodesize>
struct CBinaryTreeNode<nodesize,false>
{
bool Search(...); //顺序查找的实现
};
template<int nodesize>
struct CBinaryTreeNode<nodesize,true>
{
bool Search(...); //二分法查找的实现
};
嗯,为什么它是一个理论可行的方案呢,因为特化或者偏特化一个类,你必需全部重新实现模板类所有的函数,否则它们会“缺失”。因此我们不可能采用这种方案。
第三种方案:编译时型别机制
我们知道对于一个模板类template<int n>class CTest ,CTest<0> 和 CTest<1>是两个完全不同的类。另外我们也知道一个函数的重载只跟参数的类型有关,那么我们就实现这样的一种方案
template<bool n>
struct bool_trait
{ };
template<int nodesize,bool flag = (nodesize > critical_size) >
struct CBinaryTreeNode
{
bool Search(...)
{
Search(...,bool_trait<flag>());
}
bool Search(..., bool_trait<true> /* */) //二分法查找
{
}
bool Search(..., bool_trait<false> ) //顺序查找
{
}
};
我们第一方案中两个问题都解决了,因为在编译期就会根据nodesize计算出flag,从而决定使用何种查找方案,另外我们也可以使用完全不同的容器来实现我们的节点,可以在不同的函数中分别使用[]和GetAt来取得对应的元素。c++模板函数的特点是当没有实例化的类或者没有使用到的函数和方法都不会去进行检查,因此我们就可以顺利地通过编译了。最方便的是作为库使用者的程序员,一切都没有改变,他仍只是和以前一样
CBinaryTreeNode<100> node;
事实上这种方案也是在Modern c++ design一书中提出来的。很强大的解决方案。当然条条大路通罗马,我们还可以采用策略编程的方法,不过实在没有这个方案直观。