分类: C/C++
2008-04-02 14:42:12
Visual Studio 2005 为 Microsoft .NET 框架带来了泛型编程的类型参数化模型。当然,类型参数化是C++程序员的事情。所以,对于那些还不熟悉它们的人,我将在接下来的几期专栏里对泛型编程做一个简要的介绍。 interface class IEnumerator { property ??? Current { ??? get(); } bool MoveNext(); void Reset(); };类型系统需要你静态标识与属性的存储器回填相关的类型以及获取存取器(accessor)返回类型,但这当然是不可能的。用户需要枚举的潜在类型无以计数。你怎么办? 简单一点的常规解决办法是 UTCM,在这里对象被作为容器。 interface class IEnumerator { property Object^ Current { Object^ get(); } bool MoveNext(); void Reset(); };这样提供了一定程度上的隔离。它允许用单一不变的代码库来支持潜在的无穷多的类型。并且对于被动存储和引用类型对象的获取,其工作表现不俗。 一旦你你需要象混凝土类型那样来获取和处理该对象,事情就变得有些不那么雅致了。这要求向下强制转换为最初的对象类型。不幸的是,编译器没有必要的类型信息来保证 强制类型转换的正确性,从而造成程序员得手工显式向下转换,如下例所示: extern void f( Object^ anyTypeWorks ); Object^ o = "a string of all things"; // no downcast ... passive storage f( o ); // downcast ... we need to manipulate String^ s = safe_cast在实现集合时碰到的问题更多,因为无法静态约束某个集合在通用类型容器模型下仅容纳单一类型的对象。这只能从程序一级提供,而且稍显复杂和易错。此外,因为它是一个程序解决方案,只能被用于运行时期。 你再次得不到编译器的支持。 除了安全性和复杂性之外,还涉及大规模存储以及在通用类型容器模型下获取值类型的性能问题。借助类型参数化,这三个问题迎刃而解。 什么是参数化的类型? 类型参数模型提供了第二层隔离,消除了向下强制类型转换和框入/框出操作,并允许编译时同类容器元素类型冲突的降格(flagging)。它是一种两步解决方案。在第一步中,将一个类型参数当作一个 实际类型的占位符,就像函数所做的那样: interface class IEnumerator { property typeParameter Current { typeParameter get(); } bool MoveNext(); void Reset(); }在第二步,你告诉编译器(程序的机器阅读器),typeParameter 是一个占位而不是一个程序实体。这一步是通过叫做参数化列表的泛型署名来完成的。在C++/CLI中, 要么引入 generic 关键字以选择公共语言运行时(CLR)泛型机制,要么引入 template 关键字以选择使用 C++ 模板类型机制,如 Figure 1 所示。 这样便导致了一个类型无关的接口定义。之后,当某个类实现 IEnumerator 时,它必需提供一个实际的类型绑定到 typeParameter 占位 符。这是通过将括弧中实际的类型与参数化类名配对实现的,比如 IEnumerator C++/CLI 支持两种参数化类型机制,模板和泛型,用于定义参数化引用、值和接口类,函数和委托。从表面上看,参数化的 generic 和 template 至少在语句构成上是等同的(除了 template 或 generic 关键字有所不同)。而在其它方面,它们有显著的不同。 template默认情况下,这个表示方法被认为是一个乘法表达式:运算符 Parm::name 乘以 p。关键字 typename 的引入使程序员能重写这种默认的解释。例如, 为了声明 Parm::name 类型的指针 p,可将模板函数重写如下: template既然这个关键字的存在已经是一种既定的事实,那么非要消除因重用 class 关键字而导致的混乱是很不明智的作法。公布的代码、书籍、文章、言论、论坛和出版物 都广泛使用它,因此不能对之视而不见。这就是为什么 C++ 对这两个关键字都支持的原因。 关键字 class 或 typename 的后面是一个标识符,它在 template 或 generic 定义充当占位符。在参数列表中的每一个标识符必须唯一。但是,在 交叉声明中这两个关键字和标识符是可以改变的: template标识符的作用域用于持续类型声明的范围。在 tStack 的前向声明中,用分号结束,并且这个名字从没有被引用过。在实际定义中, 不论是在类定义中,还是在该类的每个以非内联(out-of-line)方式定义的成员函数中,这个标识符都是可见的。 类型实例化 template 或 generic 定义指明了当给定一个或多个实际类型集合时,如何构造单一的类或函数。实例化的时机是模板和泛型之间的一个主要区别之一。Template 的实例化是在编译时完成的;而 generic 的实例化是 CLR 在运行时完成的(在后面 专栏中,我会作更详细的介绍)。 template 定义做为一个自动产生特定类型实例的图解;编译器从字面上插入由用户提供的特定类型参数。而 generic 定义则更像是个蓝图;在运行时 构造特定类型实例,根据类型参数是引用还是值类型来修改常规语法。例如,用如下的代码,你可以从 template 和 generic 定义自动创建一个 int 类型的堆栈类对象和一个 String 类型的堆栈类对象: tStack这个从模板定义中产生的类被称为模板实例化——在 ISO-C++ 标准中就是这样讨论的。在泛型的文字描述中,类的生成被称为构造——这 里又看到了模板与蓝图之间的不同之处。这里,我用“实例化”来描述这一过程。当 String 类型的堆栈类被实例化时,在 generic 或 template 定义中每每出现模板参数的地方都用 String 类型取代。该类型的正确性被验证。 实例化的名称是 Stack 某个类的实例化可以在常规程序中任何使用非参数化类类型的地方使用,同样,某个实例化后的类对象的声明和使用与非参数化类完全相同。 最后,派生类和基类——绑定到独立类型参数的两个 generic(或 template)类型实例之间是没有特别关系的。认识这一点很重要。比如说,你不能在没有显式编程操作的情况下,初始化或将一个赋值给另一个。即便对整型实例化对象的非公有成员具有存取许可,你也不能进行堆栈 String 实例化操作。 本文上述内容涵盖了模板和泛型之间的共同之处。更为有趣的当然是它们的不同之外。文中我只简要地提及了一处差别——模板是在编译时进行实例化,而泛型是在运行时实例化的。这个结论实际上影响深远,它将是下一个专栏的主题。我们下回见! |
作者简介 Stanley B. Lippman 是 Microsoft 公司 Visual C++ 团队的架构师。从 1984 年开始他便在 Bell 实验室与 C++ 的发明者 Bjarne Stroustrup 一起研究 C++。在此期间,他在 Disney 和 DreamWorks 制作特色动画,同时他还是 JPL 的高级顾问以及 Fantasia 2000 的软件技术主管。 |