转换指南: 将程序从托管扩展C++迁移到C++/CLI
翻译:蒋晟
2004年8月
适用于:
C++/CLI第二版
ISO-C++
摘要: C++/CLI代表一个ISO-C++语言标准的动态范型扩展。本文列举了V1版本语言的特色 ,以及它们在V2版本中的对应(如果存在);并指出为不存在相应对应的V1特色构建的语言特性。(68打印页)
译者注:
- 原文地址在http://msdn.microsoft.com/visualc/default.aspx?pull=/library/en-us/dnvs05/html/transguide.asp(英文)。
- 本文已发表在MSDN中文网站上,网址是http://www.microsoft.com/china/MSDN/library/langtool/VCPP/TransGuide.mspx。MSDN版有所删改
- 可以在微软网站上下载C++/CLI 语言规范
- MSDN中关于Visual C++.Net的文章的聚合:http://msdn.microsoft.com/visualc/rss.xml
目录
简介
C++/CLI代表ISO-C++标准语言的一个动态编程范型扩展(dynamic programming paradigm extension). 在原版语言设计(V1)中有许多显著的弱点,我们觉得在修订版语言设计(V2)中已经修正了。 本文列举了V1版本语言的特色和它们在V2版本中的对应 (如果有这样的对应存在的话);并指出为对应不存在的V1特色构建的语言特性。对于有兴趣的读者,附录中提供新语言设计的扩展原理。 另外,一个源代码级别的转换工具(mscfront)正在开发中,而且可能在C++/CLI的发布版中提供给希望 自动化移植V1代码到新语言设计的人。
本文分为五个章节加一个附录。 第一节讨论主要的语言关键字,特别是双下划线的移除以及 上下文性和 分段关键字。 第二节着眼于托管数据类型的变化——特别是托管 引用类型和数组类型。还可以在这里找到有关确定性终结化语义 (deterministic finalization)的详细讨论。关于类成员的变化,例如属性、索引属性和操作符,是 第三节的焦点。 第四节着眼于托管枚举、内部和约束指针的语法变化。它也讨论了许多可观的语义变化,例如隐式装箱的引入、托管 CLI 枚举的变化,和对 值类默认构造函数的支持的移除。 第五节有几分像大杂烩——声名狼藉的 杂项。可以在里面找到对类型转换符号、字符串常数的行为和参数 数组的讨论。1. 语言关键字
原版到修订版的一个大体转换是在所有关键字中去掉双下划线。举例来说,一个属性现在被声明为property而不是__property。在原版设计中使用双下划线前缀的两个主要原因是:
- 这是提供符合ISO-C++标准的语言扩展的方法。原版语言设计的一个主要目的就是不引入和标准语言的不兼容性,例如新的关键字和标记。这个原因很大程度上也推动了对声明托管引用类型的对象的指针语法的选择。
- 双下划线的使用,除了兼容性方面的原因之外,也是一个不会对有旧代码基础的用户造成影响的合理保证。这是原版设计的第二主要目的。
这样的话,为什么我们移除双下划线(并且引入了一些新的标记)?不是的,这不代表我们不再考虑和标准保持一致!
我们继续致力于和标准一致。尽管如此,我们意识到对CLI动态对象模型的支持表现出了一种全新的强大的编程范型。我们在原版设计以及设计与发展C++语言的经验使我们确信,对这个新范型的支持需要它自己的高级关键字和标记。我们想提供一个新范型的一流表达方式,整合它并且支持标准语言。我们希望你会感到修订版语言设计提供了对这两种截然不同的对象模型的一流的编程体验。
类似的,我们很关心最小化这些新的关键字的对现有代码可能造成的冲击。这是用上下文性和分段关键字解决的。在我们着眼于实际语言语法的修订之前,让我们试试搞清楚这两个特别的关键字的风味。
一个上下文性关键字在特定的语言环境中有特殊的含义。例如,在通常的程序中,sealed被识别为一个普通标识符。但是,在一个托管引用类类型的声明部分,它被识别为类声明上下文中的一个关键字。这最小化了在语言中引入一个新的关键字的潜在影响,我们觉得这对我们的有旧代码基础的用户非常重要。同时,它允许新的功能的使用者获得一流的新增语言特色的体验——我们在觉得原版设计中缺少这些因素。我们将在2.1.2节中看到sealed的用法。
一个分段关键字是上下文性关键字的特例。字面上是一个上下文性修饰符和一个现存关键字配对,用空格分隔。这个配对被识别为一个语法单位,例如value class(示例参见2.1),而不是两个单独的关键字。基于现实的因素,这意味着一个重新定义value的宏,如下所示:
#ifndef __cplusplus_cli #define value
不会在一个类声明中去掉value。如果确实要这么做的话,不得不重新定义语法单位对,如下所述:
#ifndef __cplusplus_cli #define value class class
考虑到现实的因素,这是十分必要的。否则,现存的#define可能转换分段关键字的上下文性关键字部分。(译者注:例如2003年1月份的平台SDK头文件中的#define interface struct,参见拙作http://blog.joycode.com/jiangsheng/archive/2004/12/17/41283.aspx)。
2. 托管数据类型
声明托管数据类型和创建以及使用这些类型的对象的语法已经被大加修改,以提高对ISO-C++类型系统的兼容性。这些更改在后面的小节中详述。委托的讨论延后到3.3节以用类的事件成员表述它们——这是第3节的主题。(关于更加详细的跟踪引用语法介绍内幕和设计上的主要转变的讨论,参见附录 推动修订版语言设计。)
2.1 声明一个托管类类型
在原版语言定义中,一个引用类类型以__gc关键字开头。在修订版语言中,__gc关键字被两个分段关键字ref class或者ref struct之一替代。struct或者class的选择只是指明在类型体中开头未明确访问级别的部分的默认公开(对于struct)或者私有(对于class)默认访问级别。
类似地,在原版语言设计中,一个引用类类型以__value关键字开头。在修订版语言中,__value关键字被两个分段关键字value class或者value struct之一代替。
在原版语言设计中,一个接口类型,是用关键字__interface指明的。在修订版语言中,它被interface class替代。
例如,下列类声明集合
// 原版语法 public __gc class Block { ... }; // 引用类 public __value class Vector { ... }; // 值类 public __interface IMyFile { ... }; // 接口类
在修订版语言设计下等价的声明如下:
// 修订版语法 public ref class Block { ... }; public value class Vector { ... }; public interface class IMyFile { ... };
选择ref(对于引用类型)而不是gc(对于垃圾收集的)的想法是更好地暗示这个类型的本质。
2.1.1 指定一个类为抽象类型
在原版语言定义中,关键字__abstract可以被放在类型关键字之前(__gc之前或者之后)以指明该类尚未完成而且此类的对象不能在程序中创建:
public __gc __abstract class Shape {}; public __gc __abstract class Shape2D: public Shape {};
在修订版语言设计中,abstract上下文性关键字被限定在类名之后,类体、基类派生列表或者分号之前。
public ref class Shape abstract {}; public ref class Shape2D abstract : public Shape{};
当然,语义没有变化。
2.1.2 指定一个类为封闭类型
在原版语言定义中,关键字__sealed被放在类关键字之前(__gc之前或者之后)以指明类不能被继承:
public __gc __sealed class String {};
在V2语言设计中,sealed上下文性关键字限定在类名之后,类体、基类派生列表或者分号之前(你可以在声明一个继承类的同时封闭它。举例来说,String类隐式派生自Object)。封闭一个类的好处是允许静态(就是说,在编译时)解析这个密封引用类型的对象的所有的虚函数调用。这是因为密封指示符保证了String跟踪句柄不能指向一个可能重载被触发的虚方法的派生类对象。
public ref class String sealed {};
也可以将一个类既声明为抽象类也声明为封闭类。这是一种被称为静态类的特殊情况。这在CLI文档中描述如下
同时为抽象和封闭的类型只能有静态成员,并且以一些语言中调用命名空间一样的方式服务。
例如这是一个使用V1语法的抽象封闭类的声明
public __gc __sealed __abstract class State { public: static State(); static bool inParamList(); private: static bool ms_inParam; };
而这是这个声明在修订版语言设计中的译文:
public ref class State abstract sealed { public: static State(); static bool inParamList(); private: static bool ms_inParam; };
2.1.3 CLI继承: 指定基类
在CLI对象模型中,只支持公有方式的单继承。但是,在原始语言定义中仍然保留了ISO-C++对基类的解释,没有访问关键字的基类将默认成为私有派生类型。这意味着每一个CLI继承声明不得不用一个public关键字来代替默认的解释。很多用户认为编译器似乎过于严谨。
// V1:错误:默认是私有性派生 __gc class My : File{};
在修订版语言定义中,CLI继承定义缺少访问关键字时,默认是以公有的方式派生。这样,公有访问关键字就不再必要,而是可选的。虽然这个改变不需要对V1的代码做任何的修改,出于完整性考虑我仍将这个变化列出。
// V2:正确:默认是公有性派生 ref class My : File{};
2.2 一个CLI的引用类对象的声明
在原版语言定义中,一个引用类类型对象是使用ISO-C++指针语法声明的,在星号左边使用可选的__gc关键字。例如,这是V1语法下多种引用类类型对象的声明:
public __gc class Form1 : public System::Windows::Forms::Form { private: System::ComponentModel::Container __gc *components; Button __gc *button1; DataGrid __gc *myDataGrid; DataSet __gc *myDataSet; void PrintValues( Array* myArr ) { System::Collections::IEnumerator* myEnumerator = myArr->GetEnumerator(); Array *localArray = myArr->Copy(); // ... } };
在修订版语言设计中,引用类类型的对象用一个新的声明性符号(^)声明,正式的表述为跟踪句柄,更不正式的表述为帽子。(跟踪这个形容词强调了引用类型对象位于CLI堆中,因此可以透明地在堆的垃圾收集压缩过程中移动它的位置。一个跟踪句柄在运行时被透明地更新。两个类似的概念:(a)追踪引用(%)和(b)内部指针(interior_ptr<>),在第4.4.3节讨论。
声明语法不再重用ISO-C++指针语法有两个主要原因:
- 指针语法的使用不允许重载的操作符直接应用于引用对象;而不得不通过操作符的内部名称调用,例如rV1->op_Addition(rV2)而不是更加直观的rV2+Rv2。
- 许多指针操作,例如类型强制转换和指针算术对于位于垃圾收集堆上的对象无效。我们觉得一个跟踪句柄的概念最好符合一个CLI引用类型的本性。
对一个跟踪句柄使用__gc修饰符是不必要的而且是不被支持的。对象本身的用法并未变化,它仍旧通过指针成员选择操作符(->)访问成员。例如,这是上面的V1文字翻译到修订版语言语法的结果:
public ref class Form1: public System::Windows::Forms::Form{ private: System::ComponentModel::Container^ components; Button^ button1; DataGrid^ myDataGrid; DataSet^ myDataSet; void PrintValues( Array^ myArr ) { System::Collections::IEnumerator^ myEnumerator = myArr->GetEnumerator(); Array ^localArray = myArr->Copy(); // ... } };
(译者注:^引用托管堆中的整个对象,而不能用来指向类型的内部。)
2.2.1 在CLI堆上动态分配对象
在原版语言设计中,现有的在传统堆和托管堆上分配的两种new表达式很大程度上是透明的。在几乎所有的情况下,编译器能够从上下文正确决定所需的是传统堆还是托管堆。例如:
Button *button1 = new Button; // 好的: 托管堆 int *pi1 = new int; // 好的: 传统堆 Int32 *pi2 = new Int32; // 好的: 托管堆
在上下文性堆分配的结果并非所期望的行为时,可以用__gc或者__nogc关键字指引编译器。在修订版语言中,使用新引入的gcnew关键字来明显化两个new表达式的不同本质。例如,上面三个声明在修订版语言中看起来像这样:
Button^ button1 = gcnew Button; // 好的: 托管堆 int * pi1 = new int; // 好的: 传统堆 interior_ptr<Int32> pi2 = gcnew Int32; // 好的: 托管堆
(在第4节中讨论interior_ptr的更多细节。通常,它表示一个对象的地址,这个对象不必位于托管堆上。如果指向的对象确实位于托管对象堆,那么它在对象被重新定位时被透明地更新)
这是前面一节中声明的Form1的成员V1版本的初始化:
void InitializeComponent() { components = new System::ComponentModel::Container(); button1 = new System::Windows::Forms::Button(); myDataGrid = new DataGrid(); button1->Click += new System::EventHandler(this, &Form1::button1_Click); // ... }
这是用修订版语法重写的同样的初始化过程,注意引用类型是一个gcnew表达式的结果时不需要“帽子”。
void InitializeComponent() { components = gcnew System::ComponentModel::Container; button1 = gcnew System::Windows::Forms::Button; myDataGrid = gcnew DataGrid; button1->Click += gcnew System::EventHandler( this, &Form1::button1_Click ); // ... }
2.2.2 一个空的对象跟踪引用
在新的语言设计中,0不再表示一个空地址,而是被处理为一个整型,和1、10、100一样,这样我们需要引入一个特殊的标记来代表一个空值的跟踪引用。例如,在原版语言设计中,我们如下初始化一个引用类型为一个空的对象引用:
//正确:我们设置obj不引用任何对象 Object * obj = 0; //错误:没有隐式装箱 Object * obj2 = 1;
在修订版语言中,任何从值类型到一个Object的初始化或者赋值都导致一个值类型的隐式装箱(implicit boxing)。在修订版语言中,obj和obj2都被初始化为装箱过的Int32对象,分别具有值0和1。例如
//导致0和1的隐式装箱 Object ^ obj = 0; Object ^ obj2 = 1;
因此,为了允许显式的初始化、赋值一个跟踪句柄为空,以及和空指针比较,我们引入了一个新的关键字,nullptr。这样V1示例的正确的修订版看起来如下:
//好的:我们设置obj不引用任何对象 Object ^ obj = nullptr; //好的:我们初始化obj为一个Int32^ Object ^ obj2 = 1;
这使得从现存V1代码到修订版语言设计的移植稍微复杂一点。例如,考虑如下值类声明:
__value struct Holder { //原版V1语法 Holder( Continuation* c, Sexpr* v ) { cont = c; value = v; args = 0; env = 0; } private: Continuation* cont; Sexpr * value; Environment* env; Sexpr * args __gc []; };
这里args和env都是CLI引用类型。在构造函数中将他们初始化为0的语句在转移到新的语法的过程中必须修改为nullptr:
//修订版V2语法 value struct Holder { Holder( Continuation^ c, Sexpr^ v ) { cont = c; value = v; args = nullptr; env = nullptr; } private: Continuation^ cont; Sexpr^ value; Environment^ env; array<Sexpr^>^ args; };
类似的,把这些成员和0比较的测试也必须改为和nullptr比较。这是原始语法:
// 原版V1语法
Sexpr * Loop (Sexpr* input)
{
value = 0;
Holder holder = Interpret(this, input, env);
while (holder.cont != 0)
{
if (holder.env != 0)
{
holder=Interpret(holder.cont,holder.value,holder.env);
}
else if (holder.args != 0)
{
holder =
holder.value->closure()->
apply(holder.cont,holder.args);
}
}
return value;
}
而这里是修订版。转换每个0的实例到nullptr(翻译工具对这个转换有所帮助,自动处理很多——如果不是全部——的实例,包括使用NULL宏的地方。
//修订版V2语法 Sexpr ^ Loop (Sexpr^ input) { value = nullptr; Holder holder = Interpret(this, input, env); while ( holder.cont != nullptr ) { if ( holder.env != nullptr ) { holder=Interpret(holder.cont,holder.value,holder.env); } else if (holder.args != nullptr ) { holder = holder.value->closure()-> apply(holder.cont,holder.args); } } return value; }
nullptr可以转化成任何跟踪句柄类型或者指针,但是不能提升为一个整型类型。例如,在如下初始化集合中,nullptr只在开头两个初值中有效
//正确:我们设置obj和pstr不引用任何对象 Object^ obj = nullptr; char* pstr = nullptr; //在这里用0也可以 //错误:没有从nullptr到0的转换 ... int ival = nullptr;
类似的,给定一个重载过的方法集如下:
void f( Object^ ); // (1) void f( char* ); // (2) void f( int ); // (3)
一段使用nullptr的调用如下
// 错误:歧义:匹配(1)和(2) f( nullptr );
是有歧义的,因为nullptr既匹配一个跟踪句柄也匹配一个指针,而且在两者中没有一个优先选择(这需要一个显式的类型强制转换来消除歧义)。
一个使用0的调用正好匹配实例(3):
//正确:匹配(3) f( 0 );
由于0是整型。当没有f(int)的时候,它会通过一个标准转换无歧义地匹配f(char*)。在没有精确匹配时,标准转换被给与了对于值类型的隐式装箱的优先权。这是这里没有歧义的原因。
2.3 CLI数组的声明
原版语言设计中的CLI数组的声明是标准数组声明的有点不直观的扩展,一个__gc关键字放在数组对象名和可能的逗号填充的维数之间,如下一对示例所示:
// V1 语法 void PrintValues( Object* myArr __gc[]); void PrintValues( int myArr __gc[,,]);
这在修订版语言设计中被简化了,我们使用一个类似于模板的,模仿STL的向量声明。第一个参数指定元素类型。第二个参数指定数组维数(默认值是1,所以只有多维数组才需要第二个参数)。数组对象本身是一个跟踪句柄,所以必须给它一个帽子。如果元素类型也是一个引用类型,那么,它们也必须被标记。例如,上面的示例,在修订版语言中表达的时候看起来像这样:
// V2 语法 void PrintValues( array<Object^