Pure C++ 专栏...
.NET 下的泛型编程
原著:Stanley B.Lippman
翻译:许嵩罡
原文出处:Pure C++: Generic Programming Under .NET
Visual Studio 2005 为 Microsoft .NET
框架带来了泛型编程的类型参数化模型。当然,类型参数化是C++程序员的事情。所以,对于那些还不熟悉它们的人,我将在接下来的几期专栏里对泛型编程做一个简要的介绍。
泛型编程的基本思想是交付固定的代码库,这个代码库支持潜在的无限类型集合。有两种用于泛型编程的常规模型:通用类型容器模型(Universal
Type Container Model,UTCM)和类型参数化模型(Type Parameter Model,TPM)。
在 UTCM 中,与对象相关的类型信息已经被剥离。因为它是可还原的,所以容易实现。所有的对象都以一种统一的、非透明的方式存储。而在一个像 CTS(Common
Type System)这样单一的类型体系中,通用类型容器就是即 Object;所有的 CTS 类型均直接或间接地的从 Object 中派生。比如说 C 语言中,void * 就是
通用类型。
在 TPM 中,与对象关联的类型信息的绑定已经被提炼和延迟。从一个调用到另一种调用中,值的变化多种多样,它们被提炼为参数。这就是为什么这种实现模型被称为参数化
类型的原因——它更复杂,但功能强大。
例如,你在实现一个 System::Collections 名字空间的 IEnumerator
接口。只要提供两个方法和一个属性,好像很简单。但是在强类型语言中,提供对所有用户都可用的单一接口其难度是无法想象的。其难就难在我们的实现中
无法再使用“???”;
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<String^>( o );
在实现集合时碰到的问题更多,因为无法静态约束某个集合在通用类型容器模型下仅容纳单一类型的对象。这只能从程序一级提供,而且稍显复杂和易错。此外,因为它是一个程序解决方案,只能被用于运行时期。
你再次得不到编译器的支持。
除了安全性和复杂性之外,还涉及大规模存储以及在通用类型容器模型下获取值类型的性能问题。借助类型参数化,这三个问题迎刃而解。
什么是参数化的类型?
类型参数模型提供了第二层隔离,消除了向下强制类型转换和框入/框出操作,并允许编译时同类容器元素类型冲突的降格(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<int>
C++/CLI
支持两种参数化类型机制,模板和泛型,用于定义参数化引用、值和接口类,函数和委托。从表面上看,参数化的 generic 和
template 至少在语句构成上是等同的(除了 template 或 generic 关键字有所不同)。而在其它方面,它们有显著的不同。
考虑一下
Figure 2 中的两个栈声明,template 实例(tStack),通过标准模板库(STL)的 CLI 实现提供了一个使用动态
vector 容器的例子,以及 generic 实例(gStack),通过 System::Collections::Generic
名字空间提供的使用动态 List<T> 容器的例子。两者在 Visual
Studio
2005 中都是新的参数化类型集合库。
通过在类名后的尖括弧中指定实际类型来创建参数化类型实例。例如,Figure 3 依次示范了用整型和字符串类型参数实例化的 template
堆栈。为了创建等同的 generic 堆栈实例,Figure 4 所用的两种类型参数是相同的。
参数化类型对象的实际处理,例如 is 和 ss,与非参数化类型对象的处理完全一样。参数化类型的一个好处是单一的源定义能潜在地产生出无数种型实例。该例子中,generic
和 template
堆栈类在相同的参数化类源代码之外都支持字符串和整型类。在所有已知类型的应用程序中使用它们时没有真正的约束。正如你将会在后续专栏中看到的那样,并不是所有参数化类型都这样。
generic 和 template 定义以及种型实例在这里几乎是等价的,尽管并不是所有的参数化类型都这样。或者说在支持两种机制的 C++/CLI
中益处不多。当我在后续专栏中详细讨论两种机制时,你会看到其它一些差异。正是存在这些差异,在我遇到它们时,将它突出出来,而不是反复说其共性,似乎是整合其全貌的更好方法。
类型参数列表
每种类型参数都是以 class 或 typename 关键字开始的。这些关键字并包含任何平台意义——例如,class 并不是暗示要是一个本地类型,typename 也不是
意味着就是公共语言基础结构(CLI)类型。它们都表示紧跟着的名字是一个参数化类型的占位符,该占位符将会被用户指定的类型参数所取代。
之所以用中两个关键字是有历史原因的。在最初的模板规范中,Stroustrup 重用了现有的 class 关键字来指定一个类型参数而不是引入可能破坏已有程序的新关键字。直到 ISO-C++ 标准,class 关键字是声明类型参数的唯一方法。
重用现有的关键字似乎总是容易产生混淆。使用 class
来表示类型参数(parameter)是不是比内建类型和指针类型更能限制可用类型参数(arguments)成为 class
类型呢?不是,那么在这种情况下使用 class 就不会使人误解吗?肯定会的。所以,有些人觉得不引入新的关键字会导致不必要的混乱。但是那不是引入
typename 关键字的原因。
事实上,将 typename 引入 C++ 的真正的原因是为了支持模板定义的解析。这是个比较深入的话题,我在此只做一点简要介绍。详细描述请参考
Stroustrup 的 《Design and Evolution of C++》(Addison-Wesley, 1994)。
在某些情况下,要编译器来区分类型声明和表达式是不可能的。如果编译器遇到某个模板定义中的表达式Parm::name,并且 Parm 是一个表示
class 的模板类型参数,那么名称是该叫类型成员还是 Parm 的数据成员呢?
template <class Parm, class U>
Parm minus( Parm* array, U value )
{
Parm::name * p; // Is this a pointer declaration or
// a multiplication expression?
// By default treated as expression.
}
默认情况下,这个表示方法被认为是一个乘法表达式:运算符 Parm::name 乘以 p。关键字 typename 的引入使程序员能重写这种默认的解释。例如,
为了声明 Parm::name 类型的指针 p,可将模板函数重写如下:
template <class Parm, class U>
Parm minus( Parm* array, U value )
{
typename Parm::name * p; // ok: pointer declaration
}
既然这个关键字的存在已经是一种既定的事实,那么非要消除因重用 class 关键字而导致的混乱是很不明智的作法。公布的代码、书籍、文章、言论、论坛和出版物
都广泛使用它,因此不能对之视而不见。这就是为什么 C++ 对这两个关键字都支持的原因。
关键字 class 或 typename 的后面是一个标识符,它在 template 或 generic 定义充当占位符。在参数列表中的每一个标识符必须唯一。但是,在
交叉声明中这两个关键字和标识符是可以改变的:
template <class T>
public ref class tStack;
// ok: both the keyword and identifier can vary across
// declarations of the same type
template <typename elemType>
public ref class tStack {};
标识符的作用域用于持续类型声明的范围。在 tStack 的前向声明中,用分号结束,并且这个名字从没有被引用过。在实际定义中,
不论是在类定义中,还是在该类的每个以非内联(out-of-line)方式定义的成员函数中,这个标识符都是可见的。
类型实例化
template 或 generic 定义指明了当给定一个或多个实际类型集合时,如何构造单一的类或函数。实例化的时机是模板和泛型之间的一个主要区别之一。Template 的实例化是在编译时完成的;而 generic 的实例化是 CLR 在运行时完成的(在后面
专栏中,我会作更详细的介绍)。
template 定义做为一个自动产生特定类型实例的图解;编译器从字面上插入由用户提供的特定类型参数。而 generic 定义则更像是个蓝图;在运行时
构造特定类型实例,根据类型参数是引用还是值类型来修改常规语法。例如,用如下的代码,你可以从 template 和 generic 定义自动创建一个
int 类型的堆栈类对象和一个 String 类型的堆栈类对象:
tStack<int> ^si;
tStack<String^> ^ss;
这个从模板定义中产生的类被称为模板实例化——在 ISO-C++ 标准中就是这样讨论的。在泛型的文字描述中,类的生成被称为构造——这
里又看到了模板与蓝图之间的不同之处。这里,我用“实例化”来描述这一过程。当 String 类型的堆栈类被实例化时,在 generic 或
template 定义中每每出现模板参数的地方都用 String 类型取代。该类型的正确性被验证。
实例化的名称是 Stack<int>
或 Stack<String^>。紧随名字后面的 <int> 或 <String^> 符号在ISO-C++中被称为模板参数。而在 generic 表述中
则称为类型参数,本文我将遵循这样的叫法。类型参数必须在用逗号分隔的列表中指定,并用尖括号括起来。实例化的名称必须要显式指定参数类型。与函数实例化类型参数不同,用于类实例化的类型参数决不能从所使用的类实例上下文来推断——其含义将在未来关于参数化函数和函数类型的专栏中讨论。
某个类的实例化可以在常规程序中任何使用非参数化类类型的地方使用,同样,某个实例化后的类对象的声明和使用与非参数化类完全相同。
最后,派生类和基类——绑定到独立类型参数的两个 generic(或 template)类型实例之间是没有特别关系的。认识这一点很重要。比如说,你不能在没有显式编程操作的情况下,初始化或将一个赋值给另一个。即便对整型实例化对象的非公有成员具有存取许可,你也不能进行堆栈 String 实例化操作。
本文上述内容涵盖了模板和泛型之间的共同之处。更为有趣的当然是它们的不同之外。文中我只简要地提及了一处差别——模板是在编译时进行实例化,而泛型是在运行时实例化的。这个结论实际上影响深远,它将是下一个专栏的主题。我们下回见!
作者简介
Stanley B. Lippman 是 Microsoft 公司 Visual C++ 团队的架构师。从 1984 年开始他便在
Bell 实验室与 C++ 的发明者 Bjarne Stroustrup 一起研究 C++。在此期间,他在 Disney 和 DreamWorks
制作特色动画,同时他还是 JPL 的高级顾问以及 Fantasia 2000 的软件技术主管。
本文出自 MSDN Magazine 的
April 2005 期刊,可通过当地报摊获得,或者最好是
本文由 VCKBASE MTT 翻译