Stephen Toub
本文基于 Microsoft Visual Studio 2005 的预发布版本,它以前的代号为“Whidbey”。此处所包含的任何信息都可能会变更。
本文讨论:
• |
.NET C++/CLI 语法 |
• |
Interop 技术 |
• |
配置文件指导的优化 |
• |
MSIL 优化 |
• |
OpenMP 支持 |
• |
增强的缓冲区安全检查 |
本文使用了以下技术:
Visual C++ .NET 2003 和 Visual C++ 2005

对于 C++ 语言的爱好者来说,Visual Studio .NET 2003 中 C++ 编译器的引入绝对令人垂涎欲滴。Visual C++ .NET 2003 中有 98% 的部分与 ISO C++ 标准保持一致,这使它比以往任何版本更为靠近这些标准,而且它还加入了对一些功能(如局部模板专用化)的语言支持。它还包括增强的缓冲区安全检查和改进的编译器诊断功能。C++ 开发人员就像 C# 和 Visual Basic .NET 开发人员一样,可以使用拖放窗体设计器来构建健壮的 Windows 窗体应用程序。该编译器还包含了针对 Intel Pentium 4 和 AMD Athlon 处理器的优化。
如果您对 Visual C++ .NET 2003 感到兴奋不已,您将会疯狂地爱上它的下一个版本 Visual C++ 2005。Visual C++ 2005 为 .NET开发提供了既优雅又强大的新语法支持。它使用的新优化技术已经使 Microsoft 产品的运行速度提高了 30%。它通过新的编译模式来确保 Microsoft .NET 框架通用语言基础结构(Common Language Infrastructure,CIL)的一致性和可验证性,并且具有新的 interop 模型,这不仅提供了本机和托管环境的无缝合并,而且还在跨这些边界的情况下提供了完全控制。该编译器增强了前两个版本中提供的缓冲区安全检查选项,并且还包括了 C++ 应用程序普遍使用的以安全性为中心的的库的新版本。它提供了对 OpenMP 标准以及 64 位平台(其中包括 Intel Itanium 和 AMD64 芯片)的支持。它解决了混合 DLL 加载问题,并且提供了对 Double P/Invoke 性能问题的自动运行库清除。我们还可以列出许多增强和改进。正如 C++ 小组的一位架构师告诉我的,“兄弟,C++ 总算找到了属于自己的位置!”
C++/CLI 新的语法
在我们这些人中,有多少人讨厌使用前两个版本 C++ 的托管扩展语法并且认为其中尽是错误?有多少人认为 Visual C++ 没有被当作基于 .NET 的头号语言?很明显,我们当中的大多数人都是这样的(其中包括开发团队本身,您只要阅读一下他们的网络日记就知道了)。Visual C++ 小组的人听到了我们的抱怨,于是开始开发 Visual C++。与 Visual Studio .NET 2002 一起引入的 C++ 语法的托管扩展就像恐龙一样消失殆尽了,因为引入了修订的语言定义,从而产生了一种有吸引力的新语法。
设计小组对于这个版本在语言设计方面有几个重要的目标。首先(并且可能对我们这些认为代码是一种艺术的人来说最为重要),他们想要确保编程人员在编写 C++ 代码时感到很自然,而且通过对 ISO C++ 标准的纯粹扩展可以提供一种优雅的语法。他们想要让编程人员轻松地使用 C++ 编写可验证的代码来支持部分信任的情况,例如 SQL Server 2005 中的 ClickOnce 部署、窗体设计支持和托管代码宿主。他们不想为任何比 C++“更低级”的语言提供任何空间。他们想把 .NET 的全部强大功能带给 C++,而与此同时也把 C++ 的强大功能带给 .NET。他们在各个方面都取得了骄人的成功。
新的扩展规则叫做 C++/CLI,并且现在正在进行标准化。要尝试一下新的语言扩展,请参见于 2003 年 9 月 21 日在线公布的候选基本文档,它可以在C++/CLI 语言规范下载。
对任何阅读采用新语法的代码的人来说,最值得注意的就是,曾经在托管扩展中以双下划线关键字定义垃圾回收类、属性等的流行做法已经成为过去。虽然这样一些关键字仍然保留着,并且还加入了一些新的关键字,但它们现在已经不经常使用了,并且也不会影响到代码的可读性。这些双下划线的关键字由两种新类型的关键字来代替:上下文敏感的关键字和间隔排列的关键字。上下文敏感的关键字是只有在特定上下文中才使用的关键字,而间隔排列的关键字是在与其他关键字组合时才使用的关键字。例如,托管扩展中的 __property 关键字会被 property 关键字取代(不仅如此,用来定义一个属性及其访问器的全部语法都有了显著的改进,使得声明看起来非常类似于您用 C# 编写的代码。请参见图 1 中的示例)。这并不影响您在编码时将“property”用作变量的名称。在声明某一类型的属性这一上下文中,被解析为“property”的标记仅被视为一个关键字。
在新的语法中,类型以“形容词类”的形式声明,其中,形容词描述您正在创建的类是什么类型,如下所示:
class N { /*…*/ }; // native type ref class R { /*…*/ }; // CLR reference type value class V { /*…*/ }; // CLR value type interface class I { /*…*/ }; // CLR interface type enum class E { /*…*/ }; // CLR enumeration type
在之前的语言版本中,类型被声明时就可以确定它的使用范围及方式。只有本机类或结构和托管值类型可以在堆栈上创建。托管引用类总是存在于托管堆当中。在 Visual C++ 2005 中,所有的类型,无论是本机的还是托管的,都在堆栈上创建,它使用基于堆栈的确定性清理语义来完成这一功能。
要在本机堆上实例化类型 T 的一个对象,可以使用“new T”。这样就可以返回一个指向本机堆上的对象地址的指针(一个在 Visual Studio .NET 2002 和 Visual Studio .NET 2003 中称为 __nogc 指针概念)。为了在托管堆上实例化类型 T 的一个对象,Visual C++ 2005 引入了 gcnew 这一关键字,它与 new 关键字的使用方式相同。调用“gcnew T”可以返回指向托管堆中整个对象的一个句柄。句柄是在 Visual C++ 2005 中引入的一个新构造,它类似于托管扩展中的 __gc 指针。要在堆栈上实例化 T 类型的对象,标准的“T t;”声明就已经足够了。
为了公平起见,我介绍一下我是如何定义实例化的。托管引用类总是存在于托管堆当中,而本机类型总是存在于堆栈或本机堆当中。当一个托管引用被声明为存在于堆栈上时,编译器实际上还会在托管堆上对其进行实例化,如图 2 中所示。

图 2 堆栈上的托管引用类型
这样会带来一些问题。当我在堆栈上的实例超出它的使用范围时会怎样?这个实例将如何被清理掉?许多 C# 开发人员一直在抱怨 C# 语言缺少确定性清理。C# 语言提供 using 关键字来简化 IDisposable 对象的处置,但这需要额外的代码,而且与 C++ 开发人员所熟悉的析构函数的模式相比显得尤为笨拙。在 C# 中,安全的清理工作在默认情况下是无法进行的,它需要进行显式的编码。例如,请考虑图 3 中的第一个 C# 代码片断。StreamReader 对象是在托管堆上声明的。当这个方法执行完毕之后,StreamReader 的实例就没有任何引用存在了。然而,直到垃圾回收器运行时,这个对象才会被清理掉。直到那时,所用的文件才会被关闭,而在此之前,应用程序会一直占用其打开的文件句柄。要添加确定性清除,您必须使用由利用非托管资源的类实现的 IDisposable 接口。
图 3 中的第二个代码示例显示了 C# 中的新代码的外观。其实这种方法也未尝不可,而且也还算有一定的可读性。但当您开始加入更多需要清理的对象时,您的代码就会变得越来越难懂。而且,任何您忘记清理的对象都会在最后垃圾回收器实际运行时为 finalizer 线程增加负担。而与此同时,您也许已经锁定了一些有价值的资源。这一点在查看 Visual Basic .NET 中的同等实现时显得尤为不堪,同样如图 3 所示(尽管 Visual Basic 2005 增加了与 C# 相类似的 Using 语句)。
现在,Visual C++ 2005 在任何类型上都提供了可以具有析构函数和/或 finalizer 的功能,无论这种类型是托管的还是本机的。在它为托管类型的情况下,编译器会将析构函数映射到 IDisposable::Dispose 方法。这意味着您能够用 C++ 语言编写同样的方法,如图 3 中的第四个代码片断所示,其中,阅读器的析构函数/Dispose 方法将会自动被调用,就像您在 C# 中使用“Using”语句一样。当在堆栈上创建某一类型时,它的析构函数会在它超出其使用范围时被调用。
托管扩展的一个最大的问题是对指针的使用。指针被用于各种各样的任务,而其情况也是复杂多变的,因而非常难以理解。在某一特定的代码段中要解读自己在和哪一种指针打交道需要有一定程度的天赋。这种复杂性在下一个版本中会被去掉。在 Visual C++ 2005 中,指针还是原原本本的 C++ 指针。它们指向稳定的对象,而您则可以用指针进行算术操作。指向对象的指针的生命周期必须由开发人员显式管理。当使用指针时,运行库不会负责对指针带来的垃圾进行清理。
现在让我们看一下 Visual C++ 2005 的设计人员是如何解决这一问题的。与 Visual Studio .NET 2003 和 Visual Studio 2005 中使用 new 运算符返回指针不同,gcnew 运算符返回一个“句柄” — 一种新构造,在语法中用 (^) 符号来表示。.该句柄引用托管堆中的整个对象。也就是说,它们不能用来指向类型的内部,而编译器对它们的使用有许多限制,以此来强制执行这种行为,而这也可以帮助开发人员正确并安全地使用句柄。句柄不允许进行指针算术运算,也不可以被强制转换为空指针或是任何整数类型。然而,星号和箭头运算符仍被用来取消对它的引用。
这并不意味着您不能再获得一个指向垃圾回收堆上的指针。与在 C# 中组合 & 运算符与固定的关键字相似,在 Visual C++ 2005 中,pin_ptr 抽象类型允许您检索指向托管堆上对象的钉住指针。只要这个指针存在,托管堆中的对象就会被钉住,这可以防止垃圾回收器在回收的过程中移动它。Visual C++ 2005 还引入了跟踪引用运算符,用百分号 (%) 来表示。当在 C++ 中引入本机的 & 引用运算符时,大多数开发人员都知道可以把它理解成一个指向对象的指针,在使用时是由编译器来自动清除的。在大多数情况下,% 对 ^ 而言就像 & 对 * 一样。
在托管的环境下,将本机引用指向托管对象就像将本机指针指向托管对象一样危险。在指针与引用幕后的基本原理就是:被引用的对象并不会被四处移动。跟踪引用和本机引用很相似,唯一例外的是,跟踪引用引用托管堆上的对象,并且对其进行“跟踪”,即便是它们被垃圾回收器移走。百分号运算符也被用来“提取托管对象的地址”,所以就像 & 运算符在应用于本机类型时返回指向该对象的指针一样,% 运算符在应用于托管引用类型时会返回一个指向该对象的句柄。
一般来说,当 C++ 开发人员知道标准在控制它们的语言时,他们会感到心安理得。由于这个原因,为了促进第三方的采用,并确保语言向前发展的稳定性,这种新的语法采集众长而成为一个称为 C++/CLI 的提议标准。.在 2003 年 10 月,ECMA 选举出了一个特别工作组,名为 TG5,致力于分析和采用这一标准,就像 WG21 作为 ISO C++ 的管理团体一样。.实际上,WG21 中的关键人物也在 TG5 中工作。.他们的计划是在 2004 年年底将其 C++/CLI 标准化。
评论