首页 > 代码库 > 虚函数和模板编程的一点共性和特征模板的一个例子

虚函数和模板编程的一点共性和特征模板的一个例子


        最近在看元编程中,对虚函数和模板编程有一点点感悟,写一篇博客简单总结一下。

        虚函数和模板是C++里面很棒的特征,他们都提供了一种方法,让程序在编译中完成一些计算,去掉的这些计算在比较low的编程方式中,是需要在程序运行中执行的。在这里,我要强调的是:“在编译过程中完成一些计算”

       我会举两个例子,一个是虚函数的,比较简单,另一个例子是关于特征模板的,在例子中,根据模板参数的类型自动选择模板的底层数据结构。


       第一个例子是比较简单的虚函数的例子,有很多种水果的类型,我们有一个函数要展示他们的颜色。于是比较low的写法就是这样的:

struct apple {
  string color() {
    return "red";
  }
};
struct pear {
  string color() {
    return "yellow";
  }
};
void showAppleColor(const apple *a) {
  cout << a->color() << endl;
}
void showPearColor(const pear *p) {
  cout << p->color() << endl;
}
int main () {
  apple a;
  pear  p;
  showAppleColor(&a);
  showPearColor(&p);
}

但是我们都知道,有了虚函数,这个需求可以这么实现:

struct fruit {
  virtual string color() = 0;
};
struct apple : public fruit {
  string color() {
    return "red";
  }
};
struct pear : public fruit {
  string color() {
    return "yellow";
  }
};
void showYourColor(const fruit *f) {
  cout << f->color() << endl;
}
int main () {
  apple a;
  pear  p;
  showYourColor(&a);
  showYourColor(&p);
}

        其实这个例子很简单,如果对虚函数比较了解的话,就知道为什么我说"虚函数的方式,是在编译过程中完成了一些计算"。是这样的,继承了有虚函数的基类的派生类,在大部分的编译器实现中,这个类会有一个指针指向一个虚函数表,在编译过程中,编译器往虚函数表填入了真正会执行的"水果类型的函数"的地址。所以,在程序运行中,就可以通过一个基类指针实现派生类的函数调用。如果对虚函数的机制不太了解,给你推荐一篇很棒的博客:http://blog.csdn.net/haoel/article/details/1948051

       我所说的“在编译过程中完成一些计算”,也就是指编译器往虚函数表填函数地址的过程。


        第二个例子是我在工作中真实遇到的一个问题:我们要设计一个数据结构,对某一些类型的特化,底层采用特殊的数据结构,并且底层存储结构的选择要对用户代码透明。比如: 
        template<typename T> Container; 
        当T的类型是int的时候,Container采用ibis::bitvector作为底层存储,如果是其他类型,那么采用std::vector。先看一个比较low的实现:
template<typename T> 
struct Container {
  Container() {
    if(typeid(T) == typeid(int)) {
      container = new ibis::bitvector;
    } else {
      container = new std::vector<T>;
    }
  }
  // 省略析构等其他方法
  void* container;
};

       上面这个设计是可以编译通过和执行的,原理是我们是在构造函数的过程中,通过typeid来选择底层的存储类型。但是这一切都是在运行过程中完成的,噩梦很多,当你现其他方法的过程中,比如存取数据的操作,你的所有代码统统都得根据typeid来选择你的代码分支。比如:

template<typename T>
void Container::save(const T &t) {
  if (typeid(T) == typeid(int)) { // ** 是不是很恶心?其他方法的实现,都得根据typeid来做if判断
    ((ibis::bitvector *)(container))->setBit(t, 1);
  } else {
    ((std::vector<T> *)(container))->push_back(t);
  }
}

但是,有了特征模板我们可以这么实现:

// 定义两种tag
struct bitvector_tag{};
struct stdvector_tag{};

//存储选择器
//默认使用stdvector作为底层存储
template<typename T> storage_selector {
  typename stdvector_tag container_t;
};

//存储选择器
//对于int类型则底层存储用bitvector
template<int> storage_selector {
  typename bitvector_tag container_t;
};

template<typename T, typename storage> 
struct Container;

//定义stdvector的BasicContainer
template<typename T, stdvector_tag> 
struct Container {
  std::vector<T> container;
};

//定义bitvector的BasicContainer
template<typename T, bitvector_tag> 
struct Container {
  ibis::bitvector container;
};

class Book {};

int main() {
  Container<int, storage_selector<int> > c1;
  Container<Book, storage_selector<Book> > c2;
}

        在上面的代码中,我们可以看到Container特化了两个版本,template<typename T, stdvector_tag> 和 template<typename T, bitvector_tag> ,只要在用户代码中使用storage_selector,就可以选择特化版本,从而“自动”选择底层存储。统统这一切都是在编译期完成的,不管在编码难度还是在程序的大小和程序的执行效率,后者都是更加优秀的。

        我说的“编译期完成一些计算工作” 指的就是在编译过程中,根据数据类型,选择特化版本的代码,从而避开了typeid的if选择以及很多其他不必要的工作。

        这样的例子,在c++的源码里面很多很多,比如basic_string或者iterator,都有很漂亮的实现。


        虚函数和模板真是个很棒的东西,正确的使用可以带来代码良好的设计。

在编译过程完成一些计算工作,让代码更加简洁、性能更高。另外,模板元编程更是非常充分地利用了编译器的计算,值得好好研究。


虚函数和模板编程的一点共性和特征模板的一个例子