`
61party
  • 浏览: 1042248 次
  • 性别: Icon_minigender_2
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

.NET Generics vs. C++ Templates

阅读更多

.NET Generics VS C++ Templates

刘未鹏 /

C++的罗浮宫(http://blog.csdn.net/pongba)

C++中还没有引进模板的时候,C++STL之父stepanov就敏锐的发觉,面向对象理论并不能描述现有的所有结构,比如,算法就不是一个对象,再深入下去,他发觉有很多算法的抽象逻辑并不依赖于它所作用的对象以及底层数据结构的组织方式,也就是说,算法的逻辑可以抽象出来单独描述,于是他试图用当时的C++来描述这种抽象,而在当时,C++还不支持泛型,结果导致这种对算法抽象描述非常繁复而低效,与此类似的结构还有容器(算法和容器是STL的两大组成部分,两者通过迭代器“粘合”起来)。后来C++中加入了模板,不得不说很大程度上是stepanov努力的结果。这种想法在当时面向对象理论正一手遮天的情况下不得不用“天才”来形容,它直接影响了C++的“后半生”。

泛型可以在几乎无损于效率的情况下支持代码的高度可复用性,并且具有数学的抽象美(这一点接触过STL源码的人都非常清楚)。于是,泛型几乎立即就成了C++中举足轻重的特性之一。

而现在,.NETjava也意识到泛型的重要性——尽管它们有单根的特性,但是,基于继承的算法抽象描述注定是“强绑定”的,低效的,面向对象的多态的额外负担在此无可逃避。但是,基于泛型的算法抽象描述却是“非绑定”的,高效的,任何符合语法和语义要求的对象都可以作为参数传递给它,泛型的本质就是为每一种对象生成一份单独的代码(也就是所谓的“实例化”),这一份量身顶做的代码具有很高的效率,是强类型的,不需要运行期多态的支持和负担,所以.NETjava中引入泛型可以说是顺理成章的。只不过,由于.NETjava的语言本质(解释型),所以其中的泛型不能像C++那样淋漓尽致,有一些很难逾越的技术鸿沟,这在下面会提到。

下面就来说说.NET中的泛型实现。至于其语法是次要的,对C++模板稍有了解的人不难从几行示例代码中看出来。

其实,泛型的实质是相当简洁和直观的,了解宏的人很清楚,在除去类型安全的前提下,一些简单的泛型代码可以用宏来模拟。这也就是说,编译器负责为用户提供的每一集模板参数实例化出一份模板的实体。

也许你会说,这不是和C++的几乎一样吗?是的,思想是一样的,但是,实现却有本质的不同。

.NET的泛型代码的实例化是在运行期进行的,这一点与C++有本质上的不同。在.NET泛型中,处在编译期的只是一个简单的引用,而对相应的类的实例化在运行期由CLR运行层来进行,所以.NET泛型是平台的一部分,而不像C++那样,是编译器的一部分。

在运行期由CLR进行的实例化给.NET泛型带来一个显著的好处:避免代码膨胀。在C++中,位于两个不同编译单元中的模板实例化很可能导致两份完全相同的代码,而且,由两集不同模板参数实例化出来的模板实例可能无法共享某些代码。但是.NET中,实例化时CLR知道一切,所以不会出现在两个Assembly中出现两份相同的实例化代码的情况,并且,一个Assembly中实例化的代码可以被用于另一个Assembly,甚至同一个模板的不同实例间也可以共享某些代码。这种代码共享的特点可以某程度上缓解代码膨胀。

但是,.NET的泛型也有一些弱点。在运行期实例化的特点决定了它无法进行某些复杂的类型推导(这将导致JIT速度缓慢),也就是说,像STLBoost库中广泛运用的traits技术在.NET中注定要嗑磕碰碰。而支持复杂的类型推导技术的语言特性是偏特化,这也就解释了为什么.NET目前还没有支持偏特化的原因(甚至连特化也不支持)。但是,traits却又是精巧的架构和效率所必须的[1]。所以,在(运行期)效率上,.NET无法与C++比肩。

另外,.NET泛型支持constraint(约束),所谓constraint,就是给模板形参加上约束。在.NET中这些约束主要是诸如:继承自某个基类,实现了哪些接口,以及是否含有无参的public构造函数。在.NET中引入约束是有理由的:如果不引入约束,则有两种情况:第一,对于某些模板实参,实例化代码不能通过JIT编译,又因为泛型代码的编译是在运行期由CLR运行层进行的,所以彼时的编译错误可能得通过异常的方式来反映[2],然而更重要的是,这可能导致很多无谓的编译时耗,也就是说,CLR可能在编译了许多代码后发现一个编译错误,于是以前的编译都是在浪费时间。第二,解决第一种情况的方法是强制转换,例如,如果你想要对某个对象(该对象的类型是模板形参)调用CompareTo,你得先把该对象转换为ICompareable接口。这虽然能够解决问题,但是又增加了不必要的运行时开销。所以C#采用constraint,这样,如果形参不符合要求(例如,没有特定的成员函数),则在编译期就能够发现,既保证了编译期的强类型检查,又减小了运行期开销。作为比较,C++并不支持constraint,至于原因StroustrupD&E里说的很清楚。C++并不存在上面所说的问题,并且C++中的constraint可以通过其它方式来模拟[3]

从总体上来说,.NETC++“道不同”,层次也不一样,.NET中的泛型有这些简单的特性对于它已经够用了,因为.NET在运行期的信息远比C++要多(再加上单根继承的优势),所以一些工作可以交给运行期的类型识别或reflection来做(不用在编译期进行复杂的类型推导),并且.NET的语言特性相对少而简洁,这就避免了一些繁杂的工作。而C++作为编译型的语言,一经编译便几乎丧失所有结构信息和类型信息(typeinfo只包含了相当少的一部分信息),所以,C++必须在编译期打点好一切,从而,C++模板的众多特性是不可忽缺的。读者不妨将.NETdelegateBoost库中的boost::signal对比一下,看看后者为了实现一个完善的“回调”系统做了多少工作。而前者有单根结构和reflection的支持,其实现要简单得多。

所以,.NET泛型虽然简单,但却是量身定做,而C++泛型虽然复杂,却精巧而必要。二者各取所需,在各自的语言中都会有大量的用武之地。至于孰优孰劣是语言专家的事情,这里的对比是想让读者对两者有个大体的把握和理性的认识。



[1] traits的作用某中程度上意味着“类型信息的传递”以及“对于哪些类型做哪些对应的事”,这对于一个健壮的编译期类型判别系统是必须的。

[2] 事实上,假设这种存在这种情况,那么CLR抛出的异常将无法被用户catch,除非将对模板的使用放在异常try块里——这显然是不友好的。

[3] 有兴趣的读者可以参考boost库里面的相关部分。C++的这种方式可以避免增加语言特性的集合,避免过多的关键字。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics