最近在看元编程中,对虚函数和模板编程有一点点感悟,写一篇博客简单总结一下。
虚函数和模板是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,都有很漂亮的实现。
虚函数和模板真是个很棒的东西,正确的使用可以带来代码良好的设计。
在编译过程完成一些计算工作,让代码更加简洁、性能更高。另外,模板元编程更是非常充分地利用了编译器的计算,值得好好研究。