Stan Lippman:C++/CLI全景体验(转)[@more@] 最近我访问了中国的上海和北京,参加在两地举办的微软 Tech-ED 技术大会,在那里我非常荣幸地向大家介绍了我们在 C++/CLI 方面的工作。大家的反馈非常之好,特别是中国年轻一代程序员对 C++/CLI 的热爱和理解给我留下了深刻的印象。在那里,我还认识了来自上海的一位开发人员,同时也是一位技术作者, 李建忠先生。我们经过讨论之后决定合作撰写一系列 C++/CLI 方面的文章,并以“C++/CLI全景体验”专栏的形式独家授权于中国《程序员》杂志发表。这篇短文旨在为大家简单介绍一下我们写作这个专栏的一些背景 ——有点电影中“定场镜头”的味道。
面对 C++/CLI ,很多人的第一个问题自然是“什么是 C++/CLI ”,我个人喜欢将其看作是位于静态程序设计和动态程序设计之间的一座桥梁。 C++/CLI 这个名称本身就包含着一组术语——而其中最重要的术语却是最不明显的那一个。
首先来看第一个术语“ C++ ”,这当然指的是由 Bjarne Stroustrup 在 Bell 实验室时发明的 C++ 编程语言。它所支持的是一种为代码执行速度和执行体所占空间所高度优化的静态对象模型。除了堆内存分配以外,它不支持在运行时对应用程序进行任何的更改。它允许我们对底层机器进行无限的访问,但对于正在运行的程序中的活动类型、以及相关的程序基础构造,它的访问能力却非常有限、或者根本就不可能。它是一门非常成功的编程语言,但是它却不能适应目前的 Web 编程环境以及相关的安全问题——这已经成为目前程序设计中一个越来越重要的考量。
再来看第三个术语“ CLI ”,即通用语言基础构造( Common Language Infrastructure ),这是一个支持动态组件编程模型的多层架构。在许多方面,它所表示的对象模型和 C++ 的完全相反。在 CLI 中,存在一个运行时软件层(即虚拟执行环境)运行在应用程序和底层操作系统之间,应用程序代码对底层机器的访问会受到相当严格的限制;事实上, CLI 根本不允许安全环境中的代码进行这样的访问。但另一方面, CLI 却允许我们对正在运行的程序中的活动类型、以及相关的程序基础构造进行完全的访问,甚至允许我们动态构造额外的类型和程序基础构造。这些灵活性的获得当然伴随有相当的空间(执行体所占空间)和时间(程序执行效率)代价,但是它却解决了日益增长的基于连接的计算环境中所面临的问题和需要。
最后,再来看第二个术语,即中间的斜线“ / ”,它往往为人们所忽略。其表示对 C++ 和 CLI 的一种绑定( binding ),它正是 C++/CLI 设计的焦点所在。据此,对于“什么是 C++/CLI ”这一问题可能的一种答案便是“它是对静态 C++ 对象模型和动态 CLI 组件模型的一种绑定”。
对于 C++/CLI ,一个 C++ 程序员只需要将其添加到她 [ 译注 1] 已有的编程工具箱中就可以了。要成为一个 C++/CLI 程序员,你无需放弃任何已有的东西,虽然你要步入一个新的技术世界,你仍然需要学习它——但愿你能享受这一过程,至少我知道我是这样的。由此观之,我们还可以将 C++/CLI 看作是一扇通往另一个世界的大门。
C++/CLI 将动态的、基于组件的编程模型和 ISO-C++ 集成在了一起,这种集成非常类似于我们当年在 Bell 实验室对使用模板的泛型编程和当时的 C++ 所做的集成。在两种情况下,你已有的代码投资和编码经验都将得到保留。这是我们设计 C++/CLI 时一个基本的需求。
通用语言基础构造( CLI )是一个多层的体系架构,它为所有 CLI 语言提供了各种各样的服务。例如 CLI 中定义了一个通用类型系统( Common Type System ,简称 CTS ),而各个 CLI 语言都提供了自己对 CTS 的一个映射。该类型系统由一个根基类开始被组织为一个完整的类继承体系。实际上,每一个 CLI 类型都是一个类——不仅包括像 integer 、 double 这样的数值类型,而且也包括字面常量( literal constant )。每一个 CLI 类型(或者值)都表示一种 Object (所有 CLI 类型的根基类),比如数值 3.14159 、比如字符串常量 "Homer Simpson" 。
单一的根基类为运行时类型查询和代码生成(通常被称为反射)提供了支持机制 [ 译注 2] ,这是 ISO-C++ 所缺乏的。我们将在今后一系列文章中详细讨论它们给 CLI 带来的动态编程特性。
除此之外, CLI 还支持一种被称作特性元数据( attribute metadata )的构造,它允许我们定义一些特性类,然后将其关联在 CLI 类型和当前正在运行的程序构造上——这有效地扩展了内建于 CLI 中的类型和程序构造。这些用户定义的特性也可以通过反射机制来获得,应用程序则可以根据它们的值来进行条件逻辑判断。这也是 C++/CLI 为 C++ 带来的动态组件编程的一部分。再次强调一遍,类型反射和特性将在我们的专栏中得到深入的讨论。
那么,对于大家来说怎样学习 C++/CLI 呢?学习 C++/CLI 的其中一个要点便是学习底层的通用类型系统( CTS ),它包括以下三种类型:
1. 多态引用类型,其用于所有的类继承。我们将在早期的一些专栏文章中讨论它们。
2. 非多态值类型,其用于实现一些类似于数值类型那样的、对运行时效率要求比较高的类型。我们将其放在引用类型之后讨论。
3. 抽象接口类型,其用于定义一组供引用类型或者值类型实现的操作。接口为多继承提供了一种别样的设计模式。我们也将有一系列专栏文章来讨论它们。
将 CTS 映射为一组语言内置类型对于所有的 CLI 语言都适用,虽然各种语言所使用的语法各不相同。这也是一门 CLI 语言所要面对的第一个设计层面。例如,在 C# 中,我们可以用以下代码来定义一个抽象基类型 Shape (一些具体的几何对象将继承自它)。
public abstract class Shape {…}
而在 C++/CLI 中,我们用下面的代码来定义同样的类型。
public ref class Shape abstract {…};
除了语法差异之外,两种声明的实际表示完全相同。类似地,在 C# 中,我们可以用下面的代码来定义一个具体类 Point2D 。
public struct Point2D {…}
而在 C++/CLI 中,我们用下面的代码来定义同样的类型。
public value class Point2D {…};
我们对语法的选择基于如下的出发点:以一种直观的设计视角将 CLI 类型和 ISO-C++ 类型紧密地集成在一起。
因此,简单地说一种语言比另一种语言更接近底层 CLI 并不正确。相反,每一门 CLI 语言都只是表达了自己对底层 CLI 对象模型的一种视图。
学习 C++/CLI 的第二个要点是学习我们选择直接提供给程序员操作的那些底层 CLI 元素。例如, CLI 为所有语言都提供了垃圾收集服务。一门语言不能选择是否支持垃圾收集,而只能选择如何更好地提供该服务。
在 CLI 中,一个引用类型的所有对象都只能被分配在 CLI 托管堆上。这意味着 C++/CLI 支持两种动态堆——本地堆(没有任何形式的自动内存回收机制),和 CLI 托管堆。对于这两种动态堆,开发人员通常要用某种形式的 new 操作符来分配对象;如果操作成功,对象在堆中初始位置的地址将被返回。但是两者又有所区别,这是因为 CLI 托管堆中对象的位置有可能在垃圾收集器的清除以及随后的压缩中被重新调整。如果一个对象的位置被重新调整,那么 CLI 运行时中所含的其中一项服务会透明地更新所有引用该对象的指代品( thingee )。
这就使得我们面临着一种困难的选择:我们是将这些指代品称为指针,并且继续用指针的语法来表示它们呢?还是引入一种新的类似的语法来表示它们需要特殊的处理?我们最后决定采用后者,看下面的代码:
N *pn = new N;
R ^rn = gcnew R;
这里, N 表示一个本地类型,而 R 表示一个 CLI 引用类型,帽子状的符号( ^ )表示相关的地址是一个托管堆上的追踪句柄( tracking handle )——也就是说,对象位置的任何重新调整都会被 CLI 所追踪,相应的句柄也会被透明地更新。其中关键字 gcnew 在这里被用作与 CLI 托管堆打交道的 new 表达式。
值类型事实上也可以位于托管堆上,虽然这并非必须。当它们作为一个引用类型的成员时,就会出现这种情况。如果我们允许获取一个引用类型内部成员的地址,那么本地指针也是不合适的,因为这些成员的位置也需要被追踪。一种解决方法是简单地禁止该项功能。这样语言当然会变得更加简单,但是同时语言也会变得更弱——例如我们将不能通过增长元素的地址值来遍历 CLI 数组,这是因为 CLI 数组是一个引用类型,其内的元素都位于托管堆上。不提供这样的功能意味着 CLI 数组将不能适用于标准模板库( STL )中的 iterator 模式以及泛型算法。对于一个 C++ 程序员来说,这是不可接受的。
支持获取可能位于托管堆中的值类型的地址同样需要引入一种追踪指针,我们称之为追踪内部指针( tracking interior pointer )。另外,我们还支持追踪引用( tracking reference )这样的概念——它具有类似本地引用的别名语义,但是它会在必要的时候被 CLI 透明地更新。最后,我们还支持一种固定指针( pinning pointer )的概念,它可以在该指针的作用范围内阻止垃圾收集器移动其所引用的对象。
这些新的符号及其表示的复杂的间接类型是在我们对托管堆反复学习和认识之后产生的。面对生存期短暂的托管堆对象,我们需要某种精巧的方式来认识和使用它们,我们相信这些额外的间接类型可以给大家很多帮助。我们将在今后的专栏文章中详细讨论它们。
我们在此对一门 CLI 语言所选择的第二个设计层面表示了其对底层 CLI 实现模型的一层映射。选择什么样的映射取决于该编程语言定位于什么样的程序及程序员模型。当你选择一门 CLI 语言进行编程的时候,你实际上也是在选择遵从一种程序员模型。我们对于 C++/CLI 程序员的定位是那些历练较深的系统程序员,这些程序员通常所面对的任务是为高层的商业逻辑提供基础性的构造和关键性的应用,这时候她就必须要同时考虑系统的扩展性和性能,因此必须对底层 CLI 有一个系统级的视角。
学习 C++/CLI 的第三个要点是学习那些非 CLI 本身所直接提供的功能特性。这也是每一门面向 CLI 的语言所要面对的设计选择,也是各种 CLI 语言之间相互区分的一种体现。
例如, CLI 本身并不支持多类继承 (multiple class inheritance ,简称 MCI) ,而只支持多接口继承和单类继承。但 Eiffel 语言在设计其面向 CLI 的实现时就选择了支持源代码级的多类继承。这需要一种巧妙、甚至是复杂的设计将源代码级的多类继承映射为底层 CLI 的单类继承模型。 Eiffel 语言的设计人员认为这种映射对于 CLI 平台上的 Eiffel 程序员是一个利好的元素。
在此 C++/CLI 的第三个设计层面上,我们没有采用多类继承的方案。其中一个原因是我们不能说服自己多接口继承模型有任何不够简单或者优雅的地方。我们没有足够的经验来确定哪种方案绝对的优秀,但是我的直觉告诉我多类继承( MCI )是一个死胡同。我们在此设计层面上的主要关注点在于为那些 CLI 本身所欠缺的地方提供一些额外的解决方案,我们主要集中在以下三个方面:
1. 为某些 CLI 要求手动干预的地方提供一种自动化的解决方案,例如确定性终止化操作( deterministic finalization )和稀有资源释放。
2. 提供一些特殊的类成员函数——例如拷贝构造器和拷贝赋值操作符,以及在 CLI 直接支持的操作符的基础上再为一些操作符提供一些扩展支持——例如用来支持函数对象( function object )设计模式的调用操作符“()”。
3. 提供一种静态的参数化机制来支持设计适用于 CLI 类型的标准模板库( STL ),这是因为 CLI 中的泛型机制在我们来看对于当代的参数化设计是不够的——虽然我们也支持它们。
以上几点在我们的系列专栏中都将有相关的讨论。特别地,我们将会详细阐释 C++/CLI 中的模板和泛型机制。
C++/CLI 的第四个设计层面在于它选择了“集成”而非“替换”的策略,这是 C++ 以及一些语言所独有的,而其他一些语言则没有这样做,例如 Visual Basic 采取的就是“替换”的策略。一个合法的 C++ 程序是可以顺利通过 C++/CLI 编译,并且可以正常运行的。我们认为这对于我们的程序员是一项基本的需求。
谈到 C++/CLI 的第四个设计层面,这究竟是什么意思呢?它表示我们对 C++/CLI 语言规范和 ISO-C++ 所做的深入的集成。例如,除了我们扩展支持集合使其也适用于统一的 CLI 类型系统,表达式评估的标准转换集合与重载函数的辨析都和 ISO-C++ 的相同。当我们引入模板和多继承机制时,我们也应用了同样的扩展策略。这些都是在语言中稍显抽象的部分,在某种程度上我们已经使它们的行为变得更加直观,免除了程序员深入算法细节的需要。但我们仍会在系列专栏中花费笔墨关注一些主要的变化,例如对字面常量( literal )字符串的处理。
在 C++/CLI 未来的版本中,我们希望为本地类型和 CLI 类型提供更为无缝的集成。在目前的实现中,仍然存在许多不能跨越的壁垒。例如,我们现在还不能直接在一个 CLI 类中声明一个本地类的实例对象;相反,我们必须声明一个指向那个本地对象的指针,然后在 CLI 类的构造器 / 析构器对中处理对它的内存分配与释放。我们希望将来能够透明地处理它们。类似地,如果可以方便地编写下面的代码就更好了:
N^ n = gcnew N;
R* pn = new R;
即将一个本地类透明地放在垃圾收集控制的托管堆中,以及将一个 CLI 引用类型透明地放在本地堆中,并使它们正常运行。这些是我们对于 C++/CLI 未来的一些设想和愿景。随着这些设想的实现,我们也会在我们的专栏中讨论它们。
最后,再回答一个大家经常问到的一个问题,“我为什么要学习 C++/CLI ”?首要的原因是 C++/CLI 将会为你进入 CLI 所表示的动态组件编程模型领域提供一张第一等的入口签证。如果你像我一样认为这将成为越来越重要的一种编程模型,并且如果你是一个历练较深的程序员,那么 C++/CLI 就是你想要的一个语言工具。如果你不喜欢某些地方,或者发现某些东西很难表达,那么请告诉我们。我们代表着一个动态编程社区, C++/CLI 也会持续不断地前进。
在 C++/CLI 之前,如果我们希望或者需要在 CLI 所表示的动态编程领域工作,那么我们只能放弃使用 C++ [ 译注 3] ,这意味着我们同时放弃了我们现存的代码库和编码经验。有了 C++/CLI 之后,我们就拥有了一条沿着 C++ 向上的移植路径。这是学习 C++/CLI 的第一个原因。
学习 C++/CLI 的第二个原因在于它允许我们访问整个 CLI 框架类库,包括用户界面,线程,网络, XML , ADO.NET , ASP.NET ,以及 Web 服务这个宽广诱人的世界。另外,在即将推出的 WinFX 中,一个封装了整个操作系统的类库体系(包括应用程序及其执行空间 [ 译注 4] )也会被收编在 CLI 门下。
[ 译注 1 ] :在翻译 Stan Lippman 先生这篇文章的过程中,我发现 Stan 在遇到第三人称的程序员时,总是使用“ She ”、“ Her ”这样的女性代词,一开始我很困惑,因为感觉很不符合阅读习惯,但我总觉得 Stan 是有意为之。最后我决定向 Stan 询问这样做的用意。果不其然, Stan 的回答是大家习惯用“ He ”是一种男性至上主义者的体现,好像一提起程序员,大家都认为是男性。他并不认同这样的看法,特意嘱我要在翻译的文本中保留“ She ”和“ Her ”的用意,因为他反对那种老套的观点。同时还举出了两位计算机领域的女杰:软件界的先驱之一、汇编语言的创始人 Grace Hopper 博士,以及 Smalltalk 领域的重量级专家 Adele Goldberg 女士。希望 Stan 的良苦用心能够鼓励更多的女性程序员朋友来阅读我们这个专栏 J ? back
[ 译注 2 ] :单一的根基类为反射提供支持机制的理由在于反射总需要某种形式的 handle 来维护类型信息。比如在 ISO-C++ 中,这样的 handle 需要虚表来支持,如果没有虚表,就不能支持 RTTI ,这使得 ISO-C++ 对反射的支持比较弱。但 CLI 采用在一个公共的 object header 中放入一个 handle 来维护类型信息,巧妙地解决了运行时类型发现的问题。这个公共的 object header 最后就会导致所有的类型都有一个根基类——如果不是刻意隐藏该根基类的话。 back
[ 译注 3 ] :作者这里没有考虑 Managed C++ 是因为 C++/CLI 是 Managed C++ 的第二版。 back
[ 译注 4 ] :这里的“执行空间”指的是应用程序运行时的一些基础构造,如程序集、应用程序域等。
面对 C++/CLI ,很多人的第一个问题自然是“什么是 C++/CLI ”,我个人喜欢将其看作是位于静态程序设计和动态程序设计之间的一座桥梁。 C++/CLI 这个名称本身就包含着一组术语——而其中最重要的术语却是最不明显的那一个。
首先来看第一个术语“ C++ ”,这当然指的是由 Bjarne Stroustrup 在 Bell 实验室时发明的 C++ 编程语言。它所支持的是一种为代码执行速度和执行体所占空间所高度优化的静态对象模型。除了堆内存分配以外,它不支持在运行时对应用程序进行任何的更改。它允许我们对底层机器进行无限的访问,但对于正在运行的程序中的活动类型、以及相关的程序基础构造,它的访问能力却非常有限、或者根本就不可能。它是一门非常成功的编程语言,但是它却不能适应目前的 Web 编程环境以及相关的安全问题——这已经成为目前程序设计中一个越来越重要的考量。
再来看第三个术语“ CLI ”,即通用语言基础构造( Common Language Infrastructure ),这是一个支持动态组件编程模型的多层架构。在许多方面,它所表示的对象模型和 C++ 的完全相反。在 CLI 中,存在一个运行时软件层(即虚拟执行环境)运行在应用程序和底层操作系统之间,应用程序代码对底层机器的访问会受到相当严格的限制;事实上, CLI 根本不允许安全环境中的代码进行这样的访问。但另一方面, CLI 却允许我们对正在运行的程序中的活动类型、以及相关的程序基础构造进行完全的访问,甚至允许我们动态构造额外的类型和程序基础构造。这些灵活性的获得当然伴随有相当的空间(执行体所占空间)和时间(程序执行效率)代价,但是它却解决了日益增长的基于连接的计算环境中所面临的问题和需要。
最后,再来看第二个术语,即中间的斜线“ / ”,它往往为人们所忽略。其表示对 C++ 和 CLI 的一种绑定( binding ),它正是 C++/CLI 设计的焦点所在。据此,对于“什么是 C++/CLI ”这一问题可能的一种答案便是“它是对静态 C++ 对象模型和动态 CLI 组件模型的一种绑定”。
对于 C++/CLI ,一个 C++ 程序员只需要将其添加到她 [ 译注 1] 已有的编程工具箱中就可以了。要成为一个 C++/CLI 程序员,你无需放弃任何已有的东西,虽然你要步入一个新的技术世界,你仍然需要学习它——但愿你能享受这一过程,至少我知道我是这样的。由此观之,我们还可以将 C++/CLI 看作是一扇通往另一个世界的大门。
C++/CLI 将动态的、基于组件的编程模型和 ISO-C++ 集成在了一起,这种集成非常类似于我们当年在 Bell 实验室对使用模板的泛型编程和当时的 C++ 所做的集成。在两种情况下,你已有的代码投资和编码经验都将得到保留。这是我们设计 C++/CLI 时一个基本的需求。
通用语言基础构造( CLI )是一个多层的体系架构,它为所有 CLI 语言提供了各种各样的服务。例如 CLI 中定义了一个通用类型系统( Common Type System ,简称 CTS ),而各个 CLI 语言都提供了自己对 CTS 的一个映射。该类型系统由一个根基类开始被组织为一个完整的类继承体系。实际上,每一个 CLI 类型都是一个类——不仅包括像 integer 、 double 这样的数值类型,而且也包括字面常量( literal constant )。每一个 CLI 类型(或者值)都表示一种 Object (所有 CLI 类型的根基类),比如数值 3.14159 、比如字符串常量 "Homer Simpson" 。
单一的根基类为运行时类型查询和代码生成(通常被称为反射)提供了支持机制 [ 译注 2] ,这是 ISO-C++ 所缺乏的。我们将在今后一系列文章中详细讨论它们给 CLI 带来的动态编程特性。
除此之外, CLI 还支持一种被称作特性元数据( attribute metadata )的构造,它允许我们定义一些特性类,然后将其关联在 CLI 类型和当前正在运行的程序构造上——这有效地扩展了内建于 CLI 中的类型和程序构造。这些用户定义的特性也可以通过反射机制来获得,应用程序则可以根据它们的值来进行条件逻辑判断。这也是 C++/CLI 为 C++ 带来的动态组件编程的一部分。再次强调一遍,类型反射和特性将在我们的专栏中得到深入的讨论。
那么,对于大家来说怎样学习 C++/CLI 呢?学习 C++/CLI 的其中一个要点便是学习底层的通用类型系统( CTS ),它包括以下三种类型:
1. 多态引用类型,其用于所有的类继承。我们将在早期的一些专栏文章中讨论它们。
2. 非多态值类型,其用于实现一些类似于数值类型那样的、对运行时效率要求比较高的类型。我们将其放在引用类型之后讨论。
3. 抽象接口类型,其用于定义一组供引用类型或者值类型实现的操作。接口为多继承提供了一种别样的设计模式。我们也将有一系列专栏文章来讨论它们。
将 CTS 映射为一组语言内置类型对于所有的 CLI 语言都适用,虽然各种语言所使用的语法各不相同。这也是一门 CLI 语言所要面对的第一个设计层面。例如,在 C# 中,我们可以用以下代码来定义一个抽象基类型 Shape (一些具体的几何对象将继承自它)。
public abstract class Shape {…}
而在 C++/CLI 中,我们用下面的代码来定义同样的类型。
public ref class Shape abstract {…};
除了语法差异之外,两种声明的实际表示完全相同。类似地,在 C# 中,我们可以用下面的代码来定义一个具体类 Point2D 。
public struct Point2D {…}
而在 C++/CLI 中,我们用下面的代码来定义同样的类型。
public value class Point2D {…};
我们对语法的选择基于如下的出发点:以一种直观的设计视角将 CLI 类型和 ISO-C++ 类型紧密地集成在一起。
因此,简单地说一种语言比另一种语言更接近底层 CLI 并不正确。相反,每一门 CLI 语言都只是表达了自己对底层 CLI 对象模型的一种视图。
学习 C++/CLI 的第二个要点是学习我们选择直接提供给程序员操作的那些底层 CLI 元素。例如, CLI 为所有语言都提供了垃圾收集服务。一门语言不能选择是否支持垃圾收集,而只能选择如何更好地提供该服务。
在 CLI 中,一个引用类型的所有对象都只能被分配在 CLI 托管堆上。这意味着 C++/CLI 支持两种动态堆——本地堆(没有任何形式的自动内存回收机制),和 CLI 托管堆。对于这两种动态堆,开发人员通常要用某种形式的 new 操作符来分配对象;如果操作成功,对象在堆中初始位置的地址将被返回。但是两者又有所区别,这是因为 CLI 托管堆中对象的位置有可能在垃圾收集器的清除以及随后的压缩中被重新调整。如果一个对象的位置被重新调整,那么 CLI 运行时中所含的其中一项服务会透明地更新所有引用该对象的指代品( thingee )。
这就使得我们面临着一种困难的选择:我们是将这些指代品称为指针,并且继续用指针的语法来表示它们呢?还是引入一种新的类似的语法来表示它们需要特殊的处理?我们最后决定采用后者,看下面的代码:
N *pn = new N;
R ^rn = gcnew R;
这里, N 表示一个本地类型,而 R 表示一个 CLI 引用类型,帽子状的符号( ^ )表示相关的地址是一个托管堆上的追踪句柄( tracking handle )——也就是说,对象位置的任何重新调整都会被 CLI 所追踪,相应的句柄也会被透明地更新。其中关键字 gcnew 在这里被用作与 CLI 托管堆打交道的 new 表达式。
值类型事实上也可以位于托管堆上,虽然这并非必须。当它们作为一个引用类型的成员时,就会出现这种情况。如果我们允许获取一个引用类型内部成员的地址,那么本地指针也是不合适的,因为这些成员的位置也需要被追踪。一种解决方法是简单地禁止该项功能。这样语言当然会变得更加简单,但是同时语言也会变得更弱——例如我们将不能通过增长元素的地址值来遍历 CLI 数组,这是因为 CLI 数组是一个引用类型,其内的元素都位于托管堆上。不提供这样的功能意味着 CLI 数组将不能适用于标准模板库( STL )中的 iterator 模式以及泛型算法。对于一个 C++ 程序员来说,这是不可接受的。
支持获取可能位于托管堆中的值类型的地址同样需要引入一种追踪指针,我们称之为追踪内部指针( tracking interior pointer )。另外,我们还支持追踪引用( tracking reference )这样的概念——它具有类似本地引用的别名语义,但是它会在必要的时候被 CLI 透明地更新。最后,我们还支持一种固定指针( pinning pointer )的概念,它可以在该指针的作用范围内阻止垃圾收集器移动其所引用的对象。
这些新的符号及其表示的复杂的间接类型是在我们对托管堆反复学习和认识之后产生的。面对生存期短暂的托管堆对象,我们需要某种精巧的方式来认识和使用它们,我们相信这些额外的间接类型可以给大家很多帮助。我们将在今后的专栏文章中详细讨论它们。
我们在此对一门 CLI 语言所选择的第二个设计层面表示了其对底层 CLI 实现模型的一层映射。选择什么样的映射取决于该编程语言定位于什么样的程序及程序员模型。当你选择一门 CLI 语言进行编程的时候,你实际上也是在选择遵从一种程序员模型。我们对于 C++/CLI 程序员的定位是那些历练较深的系统程序员,这些程序员通常所面对的任务是为高层的商业逻辑提供基础性的构造和关键性的应用,这时候她就必须要同时考虑系统的扩展性和性能,因此必须对底层 CLI 有一个系统级的视角。
学习 C++/CLI 的第三个要点是学习那些非 CLI 本身所直接提供的功能特性。这也是每一门面向 CLI 的语言所要面对的设计选择,也是各种 CLI 语言之间相互区分的一种体现。
例如, CLI 本身并不支持多类继承 (multiple class inheritance ,简称 MCI) ,而只支持多接口继承和单类继承。但 Eiffel 语言在设计其面向 CLI 的实现时就选择了支持源代码级的多类继承。这需要一种巧妙、甚至是复杂的设计将源代码级的多类继承映射为底层 CLI 的单类继承模型。 Eiffel 语言的设计人员认为这种映射对于 CLI 平台上的 Eiffel 程序员是一个利好的元素。
在此 C++/CLI 的第三个设计层面上,我们没有采用多类继承的方案。其中一个原因是我们不能说服自己多接口继承模型有任何不够简单或者优雅的地方。我们没有足够的经验来确定哪种方案绝对的优秀,但是我的直觉告诉我多类继承( MCI )是一个死胡同。我们在此设计层面上的主要关注点在于为那些 CLI 本身所欠缺的地方提供一些额外的解决方案,我们主要集中在以下三个方面:
1. 为某些 CLI 要求手动干预的地方提供一种自动化的解决方案,例如确定性终止化操作( deterministic finalization )和稀有资源释放。
2. 提供一些特殊的类成员函数——例如拷贝构造器和拷贝赋值操作符,以及在 CLI 直接支持的操作符的基础上再为一些操作符提供一些扩展支持——例如用来支持函数对象( function object )设计模式的调用操作符“()”。
3. 提供一种静态的参数化机制来支持设计适用于 CLI 类型的标准模板库( STL ),这是因为 CLI 中的泛型机制在我们来看对于当代的参数化设计是不够的——虽然我们也支持它们。
以上几点在我们的系列专栏中都将有相关的讨论。特别地,我们将会详细阐释 C++/CLI 中的模板和泛型机制。
C++/CLI 的第四个设计层面在于它选择了“集成”而非“替换”的策略,这是 C++ 以及一些语言所独有的,而其他一些语言则没有这样做,例如 Visual Basic 采取的就是“替换”的策略。一个合法的 C++ 程序是可以顺利通过 C++/CLI 编译,并且可以正常运行的。我们认为这对于我们的程序员是一项基本的需求。
谈到 C++/CLI 的第四个设计层面,这究竟是什么意思呢?它表示我们对 C++/CLI 语言规范和 ISO-C++ 所做的深入的集成。例如,除了我们扩展支持集合使其也适用于统一的 CLI 类型系统,表达式评估的标准转换集合与重载函数的辨析都和 ISO-C++ 的相同。当我们引入模板和多继承机制时,我们也应用了同样的扩展策略。这些都是在语言中稍显抽象的部分,在某种程度上我们已经使它们的行为变得更加直观,免除了程序员深入算法细节的需要。但我们仍会在系列专栏中花费笔墨关注一些主要的变化,例如对字面常量( literal )字符串的处理。
在 C++/CLI 未来的版本中,我们希望为本地类型和 CLI 类型提供更为无缝的集成。在目前的实现中,仍然存在许多不能跨越的壁垒。例如,我们现在还不能直接在一个 CLI 类中声明一个本地类的实例对象;相反,我们必须声明一个指向那个本地对象的指针,然后在 CLI 类的构造器 / 析构器对中处理对它的内存分配与释放。我们希望将来能够透明地处理它们。类似地,如果可以方便地编写下面的代码就更好了:
N^ n = gcnew N;
R* pn = new R;
即将一个本地类透明地放在垃圾收集控制的托管堆中,以及将一个 CLI 引用类型透明地放在本地堆中,并使它们正常运行。这些是我们对于 C++/CLI 未来的一些设想和愿景。随着这些设想的实现,我们也会在我们的专栏中讨论它们。
最后,再回答一个大家经常问到的一个问题,“我为什么要学习 C++/CLI ”?首要的原因是 C++/CLI 将会为你进入 CLI 所表示的动态组件编程模型领域提供一张第一等的入口签证。如果你像我一样认为这将成为越来越重要的一种编程模型,并且如果你是一个历练较深的程序员,那么 C++/CLI 就是你想要的一个语言工具。如果你不喜欢某些地方,或者发现某些东西很难表达,那么请告诉我们。我们代表着一个动态编程社区, C++/CLI 也会持续不断地前进。
在 C++/CLI 之前,如果我们希望或者需要在 CLI 所表示的动态编程领域工作,那么我们只能放弃使用 C++ [ 译注 3] ,这意味着我们同时放弃了我们现存的代码库和编码经验。有了 C++/CLI 之后,我们就拥有了一条沿着 C++ 向上的移植路径。这是学习 C++/CLI 的第一个原因。
学习 C++/CLI 的第二个原因在于它允许我们访问整个 CLI 框架类库,包括用户界面,线程,网络, XML , ADO.NET , ASP.NET ,以及 Web 服务这个宽广诱人的世界。另外,在即将推出的 WinFX 中,一个封装了整个操作系统的类库体系(包括应用程序及其执行空间 [ 译注 4] )也会被收编在 CLI 门下。
[ 译注 1 ] :在翻译 Stan Lippman 先生这篇文章的过程中,我发现 Stan 在遇到第三人称的程序员时,总是使用“ She ”、“ Her ”这样的女性代词,一开始我很困惑,因为感觉很不符合阅读习惯,但我总觉得 Stan 是有意为之。最后我决定向 Stan 询问这样做的用意。果不其然, Stan 的回答是大家习惯用“ He ”是一种男性至上主义者的体现,好像一提起程序员,大家都认为是男性。他并不认同这样的看法,特意嘱我要在翻译的文本中保留“ She ”和“ Her ”的用意,因为他反对那种老套的观点。同时还举出了两位计算机领域的女杰:软件界的先驱之一、汇编语言的创始人 Grace Hopper 博士,以及 Smalltalk 领域的重量级专家 Adele Goldberg 女士。希望 Stan 的良苦用心能够鼓励更多的女性程序员朋友来阅读我们这个专栏 J ? back
[ 译注 2 ] :单一的根基类为反射提供支持机制的理由在于反射总需要某种形式的 handle 来维护类型信息。比如在 ISO-C++ 中,这样的 handle 需要虚表来支持,如果没有虚表,就不能支持 RTTI ,这使得 ISO-C++ 对反射的支持比较弱。但 CLI 采用在一个公共的 object header 中放入一个 handle 来维护类型信息,巧妙地解决了运行时类型发现的问题。这个公共的 object header 最后就会导致所有的类型都有一个根基类——如果不是刻意隐藏该根基类的话。 back
[ 译注 3 ] :作者这里没有考虑 Managed C++ 是因为 C++/CLI 是 Managed C++ 的第二版。 back
[ 译注 4 ] :这里的“执行空间”指的是应用程序运行时的一些基础构造,如程序集、应用程序域等。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/10617731/viewspace-957361/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/10617731/viewspace-957361/