在学习《modern c++ design》的时候,学习到了如何在 compile-time 检测两个类型之间是否可以转化.这里的转换,既包括 int,long,double 这些数据类型之间的转换,也包括基类和子类之间的转换(也就是两个类之前是否存在 class A 继承自 class B)。但是这篇文章最核心点在于 compile-time 这个限定词。因为这些检测是在 compile-time 做出的,所以不存在任何 runtime 开销。
书中的解法是使用了 sizeof 这个关键字。提到 sizeof,忍不住想要先提一下这个关键字的妙用:你可以对任意复杂的表达式使用 sizeof 关键字,最关键的一点是,sizeof 后面的表达式并不会 runtime 被执行。也就是说 sizeof(expr) 的值,在 compile-time 就已经确定下来。下面有代码佐证:
#includeint test(); int test2() { printf(__func__); return 1; } int main() { void(sizeof(test())); void(sizeof(test2())); }
上面代码可以正常的通过编译,运行后并没有任何输出,也就是没有任何函数调用。可以看到,test 函数甚至没有定义,这个性质在之后的实现中会被使用到。sizeof 的故事到此为止,下面让咱们说回正题。实现检测两个类型是否可转换使用的主要用到的技术是函数的重载和函数匹配时的类型之间的转换。其利用的主要是这样的一个事实:对于函数
int f(Base); char f(...);
f(...) 代表最低等级的转换等级,其函数匹配的优先级最低,如果 class A 可以转化为 class Base,那么
f(A());
应该是调用第一个版本,否则则为第二个版本。细心的朋友想必已经注意到,这两个函数的重载,不但重载了参数,还重载了返回类型,正是使用不同的返回类型,搭配上前面的 sizeof 关键字,为我们这篇文章提供了一个完美的解决方案。
下面贴出代码
templateclass Conversion { private: static char Test(U); static int Test(...); static T MakeT(); public: enum { exists = sizeof(Test(MakeT())) == sizeof(char) }; };
上面的代码有很多熟悉的面孔,包括最主要的函数重载和 sizeof 的使用。一个新面孔是 MakeT() 函数。这个是用来干嘛的呢?其实这个函数主要是为了兼容对于 T 的构造函数是 non-public 的情况。试想,如果将 MakeT() 调用替换为简单的 T(),那么上面的代码在遇到 T 类型的构造函数为 non-public 时,就无法奏效了,但是显然,我们并不关心这个类型的构造函数可不可以由客户端直接调用。引入了一个返回类型为 T 的函数以后,就巧妙的解决了这一点。
短短不到10行代码,我们便实现了在 compile-time 检测两个类型之间是否可以转化,而这个检测的开销基本为0,你不需要做任何函数的调用,没有任何try...catch,没有任何对象的拷贝析构。c++ 模板编程的妙用,真的让人赞叹。下面是最后的测试代码:
#include "conversion.h" #includestruct B { }; struct D : public B { }; struct T { }; int main() { using namespace std; cout << Conversion ::exists << endl; cout << Conversion ::exists << endl; cout << Conversion ::exists << endl; cout << Conversion ::exists << endl; cout << Conversion ::exists << endl; }
输出为:
1 1 0 1 0
与我们的期望完全一致。这样的技巧在 STL、boost 中都有使用。鉴于自身水平有限,文章中有难免有些错误,欢迎大家指出。也希望大家可以积极留言,与笔者一起讨论编程的那些事。