下面图老师小编跟大家分享一个简单易学的将程序从托管扩展 C++ 迁移到 C++/CLI(1)教程,get新技能是需要行动的,喜欢的朋友赶紧收藏起来学习下吧!
【 tulaoshi.com - 编程语言 】
简介C++/CLI代表 ISO-C++标准语言的一个动态编程泛型扩展 (dynamic programming paradigm extension)。在原版语言设计 (V1) 中有许多显著的弱点,我们觉得在修订版语言设计 (V2) 中已经修正了这些弱点。本文列举了 V1 版本语言的功能和它们在 V2 版本中的对应功能(如果存在);并指出了其对应功能不存在的构造。对于有兴趣的读者,可以查看附录中提供新语言设计的扩展原理。另外,一个源代码级别的转换工具 (mscfront) 正在开发中,而且可能在 C++/CLI的发布版中提供给希望将 V1 代码自动移植到新语言设计的人。
本文分为五个章节加一个附录。第一节讨论语言关键字的主要问题,特别是双下划线的移除以及与上下文相关和由空格分隔的关键字。第二节着眼于托管类型的变化 特别是托管引用类型和数组。还可以在这里找到有关确定性终结语义 (deterministic finalization) 的详细讨论。关于类成员的变化,例如属性、索引属性和操作符,是第三节的重点。第四节着眼于 CLI 枚举、内部和钉住指针的语法变化。它也讨论了许多可观的语义变化,例如隐式装箱的引入、CLI枚举的变化,和对值类中默认构造函数的支持的移除。第五节有点像大杂烩 乱七八糟的杂项。讨论了类型转换符号、字符串字符的行为和参数数组。
1. 语言关键字原版到修订版语言设计的一个重要转换是在所有关键字中去掉双下划线。举例来说,一个属性现在被声明为 property 而不是 __property。在原版语言设计中使用双下划线前缀的两个主要原因是
这是提供符合 ISO-C++标准的本地扩展的一致性方法。原版语言设计的一个主要目标就是不引入与标准语言的不兼容性,例如新的关键字和标记。这个原因很大程度上也推动了对声明托管引用类型的对象的指针语法的选择。
双下划线的使用,除了兼容性方面的原因之外,也是一个不会对有旧代码基础的用户造成影响的合理保证。这是原版语言设计的第二主要目标。
这样的话,为什么我们移除双下划线(并且引入了一些新的标记)?不是的,这并不代表我们不再考虑和标准保持一致!
我们继续致力于和标准一致。尽管如此,我们意识到对 CLI动态对象模型的支持表现出了一种全新的强大的编程范型。我们在原版语言设计上的经验以及设计与发展 C++ 语言本身的经验使我们确信,对这个新范型的支持需要它自己的高级关键字和标记。我们想提供一个该新范型的一流表达方式,整合它并且支持标准语言。我们希望您会感受到修订版语言设计提供了对这两种截然不同的对象模型的一流的编程体验。
类似的,我们很关心最小化这些新的关键字的对现有代码可能造成的冲击。这是用与上下文相关和由空格分隔的关键字来解决的。在我们着眼于实际语言语法的修订之前,让我们试试搞清楚这两个特别关键字的特点。
一个与上下文相关的关键字在特定的程序上下文中有特殊的含义。例如,在通常的程序中,sealed 是一个普通标识符。但是,在一个托管引用类类型的声明部分,它就是类声明上下文中的一个关键字。这使得在语言中引入一个新的关键字的潜在影响降到最低程度,我们认为,这对已经拥有代码基的用户非常重要。同时,它允许新功能的使用者获得一流的新增语言功能的体验 我们认为在原版语言设计中缺少这些因素。我们将在 2.1.2节中看到 sealed 用法的示例。
一个由空格分隔的关键字是与上下文相关关键字的特例。它在字面上将一个与上下文相关的修饰符和一个现存的关键字配对,用空格分隔。这个配对作为一个单独的单位,例如 value class(示例参见 1.1 节),而不是两个单独的关键字。基于现实的因素,这意味着一个重新定义 value 的宏,如下所示:
#ifndef __cplusplus_cli#define value
不会在一个类声明中去掉 value。如果确实要这么做的话,必须重新定义单元对,编写如下代码:
#ifndef __cplusplus_cli#define value class class
考虑到现实的因素,这是十分必要的。否则,现存的 #define 可能转换由空格分隔的关键字的与上下文相关的关键字部分。
2. 托管类型声明托管类型和创建以及使用这些类型的对象的语法已经大加修改,以提高 ISO-C++类型系统内的集成性。这些更改在后面的小节中详述。委托的讨论延后到 2.3节以用类中的事件成员表述它们 这是第 2 节的主题。(有关更加详细的跟踪引用语法介绍的内幕和设计上的主要转变的讨论,请参见附录A:推动修订版语言设计。)
2.1 声明一个托管类类型在原版语言定义中,一个引用类类型以 __gc关键字开头。在修订版语言中,__gc关键字被两个由空格分隔的关键字 ref class或者 ref struct之一替代。struct或者 class的选择只是指明在类型体中开头未标记部分声明的其成员的公共(对于 struct)或者私有(对于 class)默认访问级别。
类似地,在原版语言定义中,一个 value 类类型以 __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放在 class 关键字之前(__gc之前或者之后)以指明类的对象不能从以下类继承:
public __gc __sealed class String {};
在 V2语言设计中,与上下文相关的抽象关键字限定在类名之后,类体、基类派生列表或者分号之前(您可以声明一个继承类并密封它。举例来说,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; // OK: 托管堆int *pi1 = new int; // OK: 本机堆Int32 *pi2 = new Int32; // OK: 托管堆
在上下文堆分配并非所期望的实例时,可以用 __gc或者 __nogc关键字指引编译器。在修订版语言中,使用新引入的 gcnew关键字来显示两个 new 表达式的不同本质。例如,上面三个声明在修订版语言中如下所示:
Button^ button1 = gcnew Button;// OK: 托管堆 int * pi1 = new int; // OK: 本机堆 interior_ptrInt32 pi2 = gcnew Int32; // OK: 托管堆
(在第 3 节中讨论 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的初始化或者赋值都导致一个值类型的隐式装箱。在修订版语言中,obj和 obj2都被初始化为装箱过的 Int32对象,分别具有值 0和 1。例如:
//导致 0 和 1 的隐式装箱 Object ^ obj = 0; Object ^ obj2 = 1;
因此,为了允许显式的初始化、赋值,以及将跟踪句柄与空进行比较,我们引入了一个新的关键字 nullptr。这样 V1示例的正确版本如下所示:
//OK:我们设置 obj 不引用任何对象 Object ^ obj = nullptr; //OK:我们初始化 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;arraySexpr^^ 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( arrayObject^^ myArr ); void PrintValues( arrayint,3^ myArr );
因为引用类型是一个跟踪句柄而不是一个对象,所以可能将一个 CLI数组类型用于函数的返回值类型(本机数组不能用作函数返回值)。在原版语言设计中,其语法也有点不直观。例如:
// V1 语法 Int32 f() []; int GetArray() __gc[];
在 V2中,这个声明阅读和分析起来简单多了。例如:
// V2 语法 arrayInt32^ f(); arrayint^ GetArray();
本地托管数组的快捷初始化在两种版本的语言中都支持。例如
// V1 语法 int GetArray() __gc[] {int a1 __gc[] = { 1, 2, 3, 4, 5 };Object* myObjArray __gc[] = { __box(26), __box(27), __box(28), __box(29), __box(30)};// ... }
在 V2中被大大简化了(注意因为修订版语言设计中的装箱是隐式的,__box操作符被去掉了 关于其讨论参见第 3 节。
// V2 语法 arrayint^ GetArray() {arrayint^ a1 = {1,2,3,4,5}; arrayObject^^ myObjArray = {26,27,28,29,30}; // ... }
因为数组是一个 CLI引用类型,每个数组对象的声明都是一个跟踪句柄。因此,它必须在CLI堆上被分配(快捷符号隐藏了在托管堆上进行分配的细节)。以下是原版语言设计中一个数组对象的显式初始化形式:
// V1 语法 Object* myArray[] = new Object*[2]; String* myMat[,] = new String*[4,4];
回忆一下,在新的语言设计中,new表达式被 gcnew替代了。数组的维大小作为参数传递给 gcnew表达式,如下所示:
// V2 语法 arrayObject^^ myArray = gcnew arrayObject^(2); arrayString^,2^ myMat = gcnew arrayString^,2(4,4);
在修订版语言中,gcnew表达式后面可以跟一个显式的初始化列表,这在 V1语言中不被支持,例如:
// V2 语法 // explicit initialization list follow gcnew // is not supported in V1 arrayObject^^ myArray =gcnew arrayObject^(4){ 1, 1, 2, 3 }2.4 析构函数语义的变化
在原版语言定义中,类的析构函数允许存在于引用类中,但是不允许存在于值类中。这在修订的 V2语言设计中没有变化。但是,类析构函数的语义有可观的变化。怎样和为什么变化(以及这会对现存 V1代码的转换造成怎样的影响)是本节的主题。这可能是本文中最复杂的一节,所以我们慢慢来讲。这也可能是两个语言版本之间最重要的编程级别的修改,所以需要以循序渐进的方式来进行学习。
2.4.1 不确定的终止
在对象关联的内存被垃圾回收器回收之前,如果对象有一个相关的 Finalize()方法存在,那么它将被调用。您可以将该方法想象为一种超级析构函数,因为它与对象编程生命周期无关。我们称此为终止。何时甚至是否调用 Finalize()方法的计时是不确定的。这就是我们提到垃圾回收代表不确定的终止(non-deterministic finalization)时表达的意思。
不确定的终止和动态内存管理合作的很好。当可用内存缺少到一定程度的时候,垃圾回收器介入,并且很好地工作。在垃圾回收环境中,用析构函数来释放内存是不必要的。您第一次实现应用程序时不为潜在的内存泄漏发愁才怪,但是很容易就会适应了。
然而,不确定的终止机制在对象维护一个关键的资源(例如一个数据库连接或者某种类型的锁)时运转并不好。这种情况下我们需要尽快释放资源。在本机代码的环境下,这是用构造函数/析构函数对的组合解决的。不管是通过执行完毕声明对象的本机代码块还是通过由于引发异常造成的拆栈,对象的生命周期一终止,析构函数就介入并且自动释放资源。这个机制运转得很好,而且在原版语言设计中没有它的存在是一个很大的失误。
CLI提供的解决方案是实现 IDisposable接口的 Dispose()方法的类。问题是Dispose()方法需要用户显式地调用。这是个错误的倾向,因此是个倒退。C# 语言提供一个适度的自动化方式,使用一个特别的 using语句。我们的原版语言设计(我已经提到过)根本没有提供特别的支持。
2.4.2 在 V1 中,析构函数转到 Finalize()
在原版语言中,一个引用类的析构函数通过如下两步实现:
用户编写的析构函数被内部重命名为 Finalize()。如果类有一个基类(记住,在 CLI对象模型中只支持单继承),编译器在用户的代码之后插入一个对其终结器的调用。例如,给定下列 V1语言规范中的普通层次
__gc class A { public:~A() { Console::WriteLine(S"in ~A"); } }; __gc class B : public A { public:~B() { Console::WriteLine(S"in ~B"); } };
两个析构函数都被重命名为 Finalize()。B 的 Finalize()在调用 WriteLine()之后加入一个 A的 Finalize()方法的调用。这些就是垃圾回收器在终止过程中默认调用的代码。它的内部转换结果如下所示:
(本文来源于图老师网站,更多请访问http://www.tulaoshi.com/bianchengyuyan/)//V1 下析构函数的内部转换 __gc class A { public:void Finalize() { Console::WriteLine(S"in ~A"); } }; __gc class B : public A { public:void Finalize() { Console::WriteLine(S"in ~B"); A::Finalize(); } };
第二步中,编译器产生一个虚析构函数。这个析构函数就是我们的 V1用户程序直接调用或者通过 delete表达式的应用程序调用的。它永远不会被垃圾回收器调用。
这个产生的析构函数里面有什么内容呢?是两个语句。一个是调用GC::SuppressFinalize()以确保没有对 Finalize()方法的进一步调用。另一个是实际上的 Finalize()调用。回忆一下,这表达了用户提供的这个类的析构函数。如下所示:
__gc class A { public: virtual ~A() { System::GC::SuppressFinalize(this); A::Finalize(); } }; __gc class B : public A { public: virtual ~B() {System::GC:SuppressFinalize(this); B::Finalize(); } };
这个实现允许用户立刻显式调用类的 Finalize()方法,而不是随时调用,它并不真的依赖于使用 Dispose()方法的方案。这在修订版语言设计中进行了更改。
2.4.3 V2 中,析构函数转到 Dispose()
在修订版语言设计中,析构函数被内部重命名为 Dispose()方法,并且引用类自动扩展以实现 IDisposable接口。换句话说,在 V2中,这对类按如下所示进行转换:
// V2 下析构函数的内部转换 __gc class A : IDisposable { public:void Dispose() { System::GC::SuppressFinalize(this); Console::WriteLine( "in ~A"); }} }; __gc class B : public A { public:void Dispose() { System::GC::SuppressFinalize(this); Console::WriteLine( "in ~B"); A::Dispose(); } };
在 V2 中,当析构函数被显式调用时,或者对跟踪句柄应用 delete时,底层的 Dispose()方法都会自动被调用。如果这是一个派生类,一个对基类的 Dispose()方法的调用会被插入到生成方法的末尾。
但是这样也没有给我们确定性终止的方法。为了解决这个问题,我们需要局部引用对象的额外支持(在原版语言设计中没有类似的支持,所以没有转换的问题)。
2.4.4 声明一个引用对象
修订版语言支持在本地栈上声明引用类的对象,或者声明为类的成员,就像它可以直接被访问一样(注意这在 Microsoft Visual Studio 2005 的Beta1 发布版中不可用)。析构函数和在 2.4.3 节中描述的 Dispose() 方法结合时,结果就是引用类型的终止语义的自动调用。使 CLI 社区苦恼的非确定性终止这条暴龙终于被驯服了,至少对于 C++/CLI的用户来说是这样。让我们看一下这到底意味着什么。
首先,我们这样定义一个引用类,使得对象创建函数在类构造函数中获取一个资源。其次,在类的析构函数中,释放对象创建时获得的资源。
public ref class R { public: R() { /* 获得外部资源 */ } ~R(){ /* 释放外部资源 */ } // ... 杂七杂八 ... };
对象声明为局部的,使用没有附加"帽子"的类型名。所有对对象的使用(如调用成员函数)是通过成员选择点 (.) 而不是箭头 (-) 完成的。在块的末尾,转换成 Dispose()的相关的析构函数被自动调用。
void f() { R r; r.methodCall(); // ... // r被自动析构 - // 也就是说, r.Dispose() 被调用... }
相对于 C#中的 using语句来说,这只是语法上的点缀而已,而不是对基本 CLI约定(所有引用类型必须在 CLI堆上分配)的违背。基础语法仍未变化。用户可能已经编写了下面同样功能的语句(这很像编译器执行的内部转换):
(本文来源于图老师网站,更多请访问http://www.tulaoshi.com/bianchengyuyan/)// 等价的实现... // 除了它应该位于一个 try/finally 语句中之外 void f() { R^ r = gcnew R; r-methodCall(); // ... delete r; }
事实上,在修订版语言设计中,析构函数再次与构造函数配对成为和一个局部对象生命周期关联的自动获得/释放资源的机制。这个显著的成就非常令人震惊,并且语言设计者应该因此被大力赞扬。
2.4.5 声明一个显式的 Finalize()-(!R)
在修订版语言设计中,如我们所见,构造函数被合成为 Dispose()方法。这意味着在析构函数没有被显式调用的情况下,垃圾回收器在终止过程中,不会像以前那样为对象查找相关的 Finalize()方法。为了同时支持析构函数和终止,修订版语言引入了一个特殊的语法来提供一个终止器。举例来说:
public ref class R {public: !R() { Console::WriteLine( "I am the R::finalizer()!" ); }};
! 前缀表示引入类析构函数的类似符号 (~),也就是说,两种后生命周期的方法名都是在类名前加一个符号前缀。如果派生类中有一个合成的 Finalize()方法,那么在其末尾会插入一个基类的 Finalize()方法的调用。如果析构函数被显式地调用,那么终止器会被抑制。这个转换如下所示:
// V2 中的内部转换 public ref class R { public: void Finalize(){ Console::WriteLine( "I am the R::finalizer()!" ); } };
2.4.6 这在 V1 到 V2 的转换中意味着什么
这意味着,只要一个引用类包含一个特别的析构函数,一个 V1程序在 V2 编译器下的运行时行为被静默地修改了。需要的转换算法如下所示:
如果析构函数存在,重写它为类终止器方法。
如果 Dispose()方法存在,重写到类析构函数中。
如果析构函数存在,但是 Dispose()方法不存在,保留析构函数并且执行第 (1) 项。
在将代码从 V1移植到 V2的过程中,可能漏掉执行这个转换。如果应用程序某种程度上依赖于相关终止方法的执行,那么应用程序的行为将被静默地修改。
3.类或接口中的成员声明属性和操作符的声明在修订版语言设计中已经被大范围重写了,隐藏了原版设计中暴露的底层实现细节。另外,事件声明也被修改了。
在 V1中不受支持的一项更改是,静态构造函数现在可以在类外部定义了(在 V1中它们必须被定义为内联的),并且引入了委托构造函数的概念。
3.1 属性声明在原版语言设计中,每一个 set或者 get属性存取方法都被规定为一个独立的成员函数。每个方法的声明都由 __property关键字作为前缀。方法名以 set_或者 get_开头,后面接属性的实际名称(如用户所见)。这样,一个获得向量的 x坐标的属性存取方法将命名为 get_x,用户将以名称 x来调用它。这个名称约定和单独的方法规定实际上反映了属性的基本运行时实现。例如,以下是我们的向量,有一些坐标属性:
public __gc __sealed class Vector { public:// ...__property double get_x(){ return _x; }__property double get_y(){ return _y; }__property double get_z(){ return _z; }__property void set_x( double newx ){ _x = newx; }__property void set_y( double newy ){ _y = newy; }__property void set_z( double newz ){ _z = newz; } };
这使人感到迷惑,因为属性相关的函数被展开了,并且需要用户从语法上统一相关的 set 和 get。而且它在语法上过于冗长,并且感觉上不甚优雅。在修订版语言设计中,这个声明更类似于 C# property 关键字后接属性的类型以及属性的原名。set 存取和get 存取方法放在属性名之后的一段中。注意,与 C# 不同,存取方法的符号被指出。例如,以下是上面的代码转换为新语言设计后的结果:
public ref class Vector sealed { public:property double x { double get() { return _x; } void set( double newx ) { _x = newx; }} // Note: no semi-colon ... };
如果属性的存取方法表现为不同的访问级别 例如一个公有的 get和一个私有的或者保护的 set,那么可以指定一个显式的访问标志。默认情况下,属性的访问级别反映了它的封闭访问级别。例如,在上面的 Vector定义中,get和 set方法都是公有的。为了让 set方法成为保护或者私有的,必须如下修改定义:
public ref class Vector sealed { public:property double x { double get() { return _x; }private: void set( double newx ) { _x = newx; }} // 注意:private 的作用域到此结束 ...//注意:dot 是一个 Vector 的公有方法...double dot( const Vector^ wv ); // etc. };
属性中访问关键字的作用域延伸到属性的结束括号或者另一个访问关键字的说明。它不会延伸到属性的定义之外,直到进行属性定义的封闭访问级别。例如,在上面的声明中,Vector::dot()是一个公有成员函数。
为三个 Vector坐标编写 set/get属性有点乏味,因为实现的本质是定死的:(a) 用适当类型声明一个私有状态成员,(b) 在用户希望取得其值的时候返回,以及 (c) 将其设置为用户希望赋予的任何新值。在修订版语言设计中,一个简洁的属性语法可以用于自动化这个使用方式:
public ref class Vector sealed { public://等价的简洁属性语法property double x; property double y; property double z; };
简洁属性语法所产生的一个有趣的现象是,在编译器自动生成后台状态成员时,除非通过 set/get访问函数,否则这个成员在类的内部不可访问。这就是所谓的严格限制的数据隐藏!
3.2 属性索引声明原版语言对索引属性的支持的两大缺点是不能提供类级别的下标,也就是说,所有索引属性必须有一个名字,举例来说,这样就没有办法提供可以直接应用到一个 Vector或者Matrix类对象的托管下标操作符。其次,一个次要的缺点是很难在视觉上区分属性和索引属性 参数的数目是唯一的判断方法。最后,索引属性具有与非索引属性同样的问题 存取函数没有作为一个基本单位,而是分为单独的方法。举例来说:
public __gc class Vector; public __gc class Matrix { float mat[,]; public: __property void set_Item( int r, int c, float value);__property int get_Item( int r, int c );__property void set_Row( int r, Vector* value );__property int get_Row( int r ); };
如您所见,只能用额外的参数来指定一个二维或者一维的索引,从而区分索引器。在修订版语法中,索引器由名字后面的方括号 ([,]) 区分,并且表示每个索引的数目和类型:
public ref class Vector; public ref class Matrix { private:arrayfloat, 2^ mat; public:property int Item [int,int]{ int get( int r, int c ); void set( int r, int c, float value );}property int Row [int]{ int get( int r ); void set( int r, Vector^ value );} };
在修订版语法中,为了指定一个可以直接应用于类对象的类级别索引器,重用 default关键字以替换一个显式的名称。例如:
public ref class Matrix{private: arrayfloat, 2^ mat;public: //OK,现在有类级别的索引器了 // // Matrix mat ... // mat[ 0, 0 ] = 1;// // 调用默认索引器的 set 存取函数... property int default [int,int] { int get( int r, int c ); void set( int r, int c, float value ); } property int Row [int] { int get( int r ); void set( int r, Vector^ value ); }};
在修订版语法中,当指定了 default索引属性时,下面两个名字被保留:get_Item和set_Item。这是因为它们是 default索引属性产生的底层名称。
注意,简单索引语法与简单属性语法截然不同。
3.3 委托和事件声明一个委托和普通事件仅有的变化是移除了双下划线,如下面的示例所述。在去掉了之后,这个更改被认为是完全没有争议的。换句话说,没有人支持保持双下划线,所有人现在看来都同意双下划线使得原版语言感觉很难看。
// 原版语言 (V1) __delegate void ClickEventHandler(int, double); __delegate void DblClickEventHandler(String*); __gc class EventSource { __event ClickEventHandler* OnClick;__event DblClickEventHandler* OnDblClick; // ... }; // 修订版语言 (V2) delegate void ClickEventHandler( int, double ); delegate void DblClickEventHandler( String^ ); ref class EventSource {event ClickEventHandler^ OnClick; event DblClickEventHandler^ OnDblClick; // ... };
事件(以及委托)是引用类型,这在 V2中更为明显,因为有帽子 (^) 的存在。除了普通形式之外,事件支持一个显式的声明语法,用户显式指定事件关联的 add()、raise()、和 remove()方法。(只有 add()和 remove()方法是必须的;raise()方法是可选的)。
在 V1设计中,如果用户选择提供这些方法,尽管她必须决定尚未存在的事件的名称,她也不必提供一个显式的事件声明。每个单独的方法以 add_EventName、raise_EventName、和 remove_EventName的格式指定,如以下引用自 V1语言规范的示例所述:
// 原版 V1 语言下 // 显式地实现 add、remove 和 raise ... public __delegate void f(int); public __gc struct E {f* _E; public:E() { _E = 0; }__event void add_E1(f* d) { _E += d; }static void Go() { E* pE = new E; pE-E1 += new f(pE, &E::handler); pE-E1(17);pE-E1 -= new f(pE, &E::handler); pE-E1(17); } private:__event void raise_E1(int i) { if (_E) _E(i);} protected:__event void remove_E1(f* d) { _E -= d;} };
该设计的问题主要是感官上的,而不是功能上的。虽然设计支持添加这些方法,但是上面的示例看起来并不是一目了然。因为 V1属性和索引属性的存在,类声明中的方法看起来千疮百孔。更令人沮丧的是缺少一个实际的 E1事件声明。(再强调一遍,底层实现细节暴露了功能的用户级别语法,这显然增加了语法的复杂性。)这只是劳而无功。V2设计大大简化了这个声明,如下面的转换所示。事件在事件声明及其相关委托类型之后的一对花括号中指定两个或者三个方法如下所示:
// 修订版 V2 语言设计 delegate void f( int ); public ref struct E { private:f^ _E; //是的,委托也是引用类型 public:E(){ // 注意 0 换成了 nullptr! _E = nullptr; }// V2 中显式事件声明的语法聚合event f^ E1{public: void add( f^ d ) { _E += d; }protected: void remove( f^ d ) { _E -= d; }private: void raise( int i ) { if ( _E ) _E( i ); }}static void Go(){ E^ pE = gcnew E; pE-E1 += gcnew f( pE, &E::handler ); pE-E1( 17 );pE-E1 -= gcnew f( pE, &E::handler ); pE-E1( 17 ); } };
虽然在语言设计方面,人们因为语法的简单枯燥而倾向于忽视它,但是如果对语言的用户体验有很大的潜移默化的影响,那么它实际上很有意义。一个令人迷惑的、不优雅的语法可能增加开发过程的风险,很大程度上就像一个脏的或者不清晰的挡风玻璃增加开车的风险一样。在修订版语言设计中,我们努力使语法像一块高度磨光的新安装的挡风玻璃一样透明。
3.4 密封一个虚函数__sealed关键字在 V1版中用于修饰一个引用类型,禁止从此继续派生 如 2.1.2 节所述 或者修饰一个虚函数,禁止从派生类中继续重写方法。举例来说:
class base { public: virtual void f(); }; class derived : public base { public:__sealed void f(); };
在此示例中,derived::f()根据函数原型的完全匹配来重写 base::f()实例。__sealed关键字指明一个继承自 derived类的后续类不能重写 derived::f()。
在新的语言设计中,sealed放在符号之后,而不是像在 V1 中那样,允许放在实际函数原型之前任何位置。另外,sealed的使用也需要显式使用 virtual关键字。换句话说,上面的 derived的正确转换如下所述:
class derived: public base { public:virtual void f() sealed; };
缺少 virtual关键字会产生一个错误。在 V2中,上下文关键字 abstract可以在 =0 处用来指明一个纯虚函数。这在 V1中不被支持。举例来说:
class base { public: virtual void f()=0; };
可以改写为
class base { public: virtual void f() abstract; };3.5 操作符重载
原版语言设计最惊人之处可能是它对于操作符重载的支持 或者更恰当地说,是有效的缺乏支持。举例来说,在一个引用类型的声明中,不是使用内建的 operator+语法,而是必须显式编写出操作符的底层内部名称 在本例中是 op_Addition。但更加麻烦的是,操作符的调用必须通过该名称来显式触发,这样就妨碍了操作符重载的两个主要好处:(a) 直观的语法,和 (b) 混合现有类型和新类型的能力。举例来说:
public __gc __sealed class Vector { public: Vector( double x, double y, double z ); static boolop_Equality( const Vector*, const Vector* ); static Vector* op_Division( const Vector*, double ); static Vector* op_Addition( const Vector*, const Vector* ); static Vector* op_Subtraction( const Vector*, const Vector* ); }; int main() { Vector *pa = new Vector( 0.231, 2.4745, 0.023 ); Vector *pb = new Vector( 1.475, 4.8916, -1.23 );Vector *pc1 = Vector::op_Addition( pa, pb ); Vector *pc2 = Vector::op_Subtraction( pa, pc1 ); Vector *pc3 = Vector::op_Division( pc1, pc2-x() ); if ( Vector::op_Equality( pc1, p2 )) // ... }
在语言的修订版中,满足了传统 C++程序员的普通期望,声明和使用静态操作符。以下是转换为 V2语法的 Vector类:
public ref class Vector sealed { public:Vector( double x, double y, double z );static booloperator ==( const Vector^, const Vector^ );static Vector^ operator /( const Vector^, double );static Vector^ operator +( const Vector^, const Vector^ );static Vector^ operator -( const Vector^, const Vector^ ); }; int main() {Vector^ pa = gcnew Vector( 0.231, 2.4745, 0.023 ),Vector^ pb = gcnew Vector( 1.475,4.8916,-1.23 );Vector^ pc1 = pa + pb;Vector^ pc2 = pa-pc1;Vector^ pc3 = pc1 / pc2-x();if ( pc1 == p2 ) // ... }3.6 转换操作符
谈到令人不愉快的感觉,在 V1语言设计中必须编写 op_Implicit来指定一个转换感觉上就不像 C++。例如,以下是引自 V1语言规范的 MyDouble定义:
__gc struct MyDouble {static MyDouble* op_Implicit( int i ); static int op_Explicit( MyDouble* val );static String* op_Explicit( MyDouble* val ); };
这就是说,给定一个整数,将这个整数转换为 MyDouble的算法是通过op_Implicit操作符实现的。进一步说,这个转换将被编译器隐式执行。类似的,给定一个 MyDouble对象,两个 op_Explicit操作符分别提供了以下两种算法:将对象转换为整型或者托管字符串实体。但是,编译器不会执行这个转换,除非用户显式要求。
在 C#中,如下所示:
class MyDouble {public static implicit operator MyDouble( int i ); public static explicit operator int( MyDouble val );public static explicit operator string( MyDouble val ); };
除了每个成员都有的显式公有 访问标志看起来很古怪,C#代码看起来比 C++的托管扩展更加像 C++。所以我们不得不修复这个问题。但是我们怎么才能做到?
一方面,C++程序员将构建为转换操作符单参数构造函数省略掉。但是,另一方面,该设计被证明是如此难于处理,以致于 ISO-C++委员会引入了一个关键字 explicit,只是为了处理它的意外后果 例如,有一个整型参数作为维数的 Array类隐式地将任何整型变量转换为 Array对象,甚至在用户最不需要时也这样。Andy Koenig 是第一个引起我注意的人,他解释了一个设计习惯,构造函数中的第二虚参数只是用来阻止这种不好的事情的发生。所以我不会对 C++/CLI中缺乏单构造函数隐式转换而感到遗憾。
另一方面,在 C++中设计一个类类型时提供一个转换对从来不是一个好主意。这方面最好的示例是标准 string类。隐式转换是有一个 C风格字符串的单参数构造函数。但是,它没有提供一个对应的隐式转换操作符来将 string 对象转换为 C风格的字符串 而是需要用户显式调用一个命名函数 在这个示例中是 c_str()。
这样,将转换操作符的隐式/显式行为进行关联(以及将一组转换封装到一组声明)看起来是原始 C++ 对转换操作符支持的改进,这个支持自从 1988 年 Robert Murray 发布了关于 UsenixC++的标题为 Building Well-Behaved Type Relationships in C++的讲话之后,已经成为一个公开的警世篇,讲话最终产生了 explicit 关键字。修订版 V2语言对转换操作符的支持如下所示,比 C# 的支持稍微简略一点,因为操作符的默认行为支持隐式转换算法的应用:
ref struct MyDouble { public:static operator MyDouble^ ( int i );static explicit operator int ( MyDouble^ val );static explicit operator String^ ( MyDouble^ val ); };
V1到 V2的另一个变化是,V2中的单参数构造函数以声明为 explicit 的方式处理。这意味着为了触发它的调用,需要一个显式的转换。但是要注意,如果一个显式的转换操作符已经定义,那么是它而不是单参数构造函数会被调用。
3.7 接口成员的显式重写经常有必要在实现接口的类中提供两个接口成员的实例 一个用于通过接口句柄操作类对象,另一个用于通过类界面使用对象。例如:
public __gc class R : public ICloneable {// 通过ICloneable使用... Object* ICloneable::Clone();// 通过一个R对象使用 ...R* Clone(); };
在 V1中,我们通过一个用接口名限定的方法名来提供接口方法的显式声明,从而解决这个问题。特定于类的实例是未被限定的。在这个示例中,当通过 R的一个实例显式调用 Clone()时,这样可以免除对其返回值的类型向下强制转换。
在 V2中,一个通用重写机制被引入,用来替换前面的语法。我们的示例会被重写,如下所示:
public ref class R : public ICloneable {// 通过 ICloneable 使用 ... Object^ InterfaceClone() = ICloneable::Clone;// 通过一个 R 对象使用 ...virtual R^ Clone() new; };
这个修订要求为显式重写的接口成员赋予一个在类中唯一的名称。这里我提供了一个有些笨拙的名称 InterfaceClone()。修订版的行为仍旧是相同的 通过 ICloneable接口的调用触发重命名的 InterfaceClone(),而通过 R 类型对象的调用调用第二个 Clone()实例。
3.8 私有虚函数在 V1中,虚函数的访问级别并不影响它在派生类中是否可以被重写。这在 V2中被修改了。在 V2中,虚函数不能重写不可访问的基类虚函数。例如:
__gc class My{ //在派生类中无法访问...virtual void g();};__gc class File : public My {public: // 正确:在 V 1中,g() 重写了 My::g() // 错误:在 V2 中,不能重写: My::g() 无法访问...void g();};
对于这种设计而言,实际上没有在 V2中的对应。要重写这个函数,必须使基类的成员可访问 也就是说,非私有的。继承的方法不必沿用同样的访问级别。在这个示例中,最小的改变是将 My成员声明为保护的。这样,一般的程序通过 My来访问这个方法仍旧是被禁止的。
ref class My { protected: virtual void g(); }; ref class File : My { public: void g(); };
注意在 V2 下,如果基类缺少显式的 virtual关键字,那么会产生一个警告消息。
3.9 静态常量整型的连接方式 (linkage) 不再是 literal 的虽然 static const整型成员仍旧被支持,但是它们的 linkage 属性被修改了。以前的 linkage 属性现在通过一个 literal整型成员来完成。例如,考虑如下 V1类:
public __gc class Constants { public: static const int LOG_DEBUG = 4; // ... };
它为域产生如下的底层 CIL属性(注意黑体的 literal 属性):
.field public static literal int32 modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier) STANDARD_CLIENT_PRX = int32(0x00000004)
它虽然在 V2 语法下仍旧可以编译,
public ref class Constants { public: static const int LOG_DEBUG = 4; // ... };
但是不再产生 literal属性,所以不被 CLI运行库视为一个常量。
.field public static int32 modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier) STANDARD_CLIENT_PRX = int32(0x00000004)
为了具有同样的中间语言的 literal属性,声明应该改为使用新支持的 literal数据成员,如下所示:
public ref class Constants { public: literal int LOG_DEBUG = 4; // ... };4 值类型及其行为
本节中我们着眼于 CLI枚举类型和值类类型,同时研究装箱和对 CLI堆上装箱实例的访问,以及考虑内部和钉住指针。这个领域的语言变化范围很广。
4.1 CLI 枚举类型原版语言的 CLI枚举声明前有一个 __value 关键字。 这里的意图是区分本机枚举和派生自 System::ValueType 的 CLI枚举,同时暗示它们具有同样的功能。例如,
__value enum e1 { fail, pass }; public __value enum e2 : unsigned short { not_ok = 1024, maybe, ok = 2048 };
修订版语言用强调后者的类本质而不是其值类型本源的方法来解决这个区分本机枚举和 CLI枚举的问题。同样,__value关键字被废弃了,替换成了一对由空格分隔的关键字 enum class。这实现了引用类、值类和接口类声明中关键字对的对称。
enum class ec; value class vc; ref class rc; interface class ic;
修订版语言设计中的枚举对 e1 和 e2的转换如下所示:
enum class e1 { fail, pass }; public enum class e2 : unsigned short { not_ok = 1024, maybe, ok = 2048 };
除了这种句法上的小小修改之外,托管枚举类型的行为在很多方面有所改变:
CLI枚举的前置声明在 V2中不再支持。V2 中没有这样的对应。这只会导致编译时错误。
__value enum status; // V1: 正确 enum class status; // V2: 错误
在内建算术类型和对象类层次结构之间进行重载解析的次序在 V2和 V1中被颠倒了!一个副作用是,托管枚举在 V2 中不能再像在 V1中一样隐式转换成算术类型。
与在 V1 中不同,在 V2中,托管枚举具有它自己的范围。在 V1中,枚举数在包含枚举的范围内可见。在 V2中,枚举数被限定在枚举的范围内。
4.1.1 CLI 枚举是一种类型举例来说,考虑如下代码片断:
__value enum status { fail, pass }; void f( Object* ){ cout "f(Object)n"; } void f( int ){ cout "f(int)n"; } int main() {status rslt;// ...f( rslt ); // which f is invoked? }
对于本机 C++程序员来说,该问题自然的答案是,被调用的重载 f()的实例是 f(int)。枚举是一个整型符号常量,并且在此示例中作为标准整型被转换。实际上,在原版语言设计中,这事实上就是调用解析的实例。这产生了一些意想不到的结果 不是在我们以本机 C++框架思想使用它的时候 而是在我们需要它们与现存的 BCL(基类库)框架交互的时候,这里枚举是一个间接派生自 Object的类。在修订版语言设计中,被调用的 f()实例是 f(Object^)。
V2选择强制不支持 CLI枚举和算术类型之间的隐式转换。这意味着任何从托管枚举类型对象到算术类型的赋值都需要一个显式的强制转换。举例来说,假定
void f( int );
是一个非重载方法,在 V1中,调用
f( rslt ); // ok: V1; error: V2
是可行的,rslt 中的值被隐式转换为一个整型值。在 V2中,这个调用的编译会失败。要正确转换它,我们必须插入一个转换操作符:
f( safe_castint( rslt )); // ok: V2
4.1.2 CLI 枚举类型的范围
C和 C++语言之间的不同之一就是 C++在 struct 中添加了范围。在 C中,struct 只是一个数据的聚合,既不支持接口也不支持关联的范围。这在当时是一个十分激进的改变,并且对于很多从 C 语言转移过来的新 C++ 用户来说是一个有争议的问题。本机和 CLI 的枚举的关系也类似。
在原始语言设计中,曾经尝试过为托管枚举的枚举数定义弱插入名称,用于模拟本机枚举内范围的缺失。这个尝试被证明是失败的,问题在于这造成了枚举数溢出到全局命名空间,造成了管理名称冲突的困难。在修订版语言中,我们按照其他 CLI语言来支持托管枚举的范围。
这意味着 CLI 枚举的枚举数的任何未限定使用将不能被修订版语言识别。让我们来看一个实际的例子。
// 原版语言设计支持弱插入 __gc class XDCMake { public: __value enum _recognizerEnum { UNDEFINED, OPTION_USAGE, XDC0001_ERR_PATH_DOES_NOT_EXIST = 1, XDC0002_ERR_CANNOT_WRITE_TO = 2, XDC0003_ERR_INCLUDE_TAGS_NOT_SUPPORTED = 3, XDC0004_WRN_XML_LOAD_FAILURE = 4, XDC0006_WRN_NONEXISTENT_FILES = 6, }; ListDictionary* optionList; ListDictionary* itagList; XDCMake() { optionList = new ListDictionary; // here are the problems ... optionList-Add(S"?", __box(OPTION_USAGE)); // (1) optionList-Add(S"help", __box(OPTION_USAGE)); // (2) itagList = new ListDictionary; itagList-Add(S"returns", __box(XDC0004_WRN_XML_LOAD_FAILURE)); // (3) } };
三个枚举数名称的未限定使用 ((1)、(2)和(3)) 都需要在转换为修订版语言语法时被限定,从而让源代码通过编译。以下是原始源代码的正确转换:
ref class XDCMake { public: enum class _recognizerEnum { UNDEFINED, OPTION_USAGE, XDC0001_ERR_PATH_DOES_NOT_EXIST = 1, XDC0002_ERR_CANNOT_WRITE_TO = 2, XDC0003_ERR_INCLUDE_TAGS_NOT_SUPPORTED = 3, XDC0004_WRN_XML_LOAD_FAILURE = 4, XDC0006_WRN_NONEXISTENT_FILES = 6 }; ListDictionary^ optionList; ListDictionary^ itagList; XDCMake() { optionList = gcnew ListDictionary; optionList-Add("?",_recognizerEnum::OPTION_USAGE); // (1) optionList-Add("help",_recognizerEnum::OPTION_USAGE); //(2) itagList = gcnew ListDictionary; itagList-Add( "returns", recognizerEnum::XDC0004_WRN_XML_LOAD_FAILURE); //(3) } };
这改变了本机和 CLI 枚举之间的设计策略。因为 CLI 枚举在 V2中保持一个关联的范围,在一个类中封装枚举的声明不再是有必要和有效的了。这个用法随着贝尔实验室的 cfront 2.0而不断发展,也用来解决全局名称污染的问题。
在贝尔实验室的 Jerry Schwarz 所创建的 beta 原版新 iostream库中,Jerry 没有封装库中定义的全部相关枚举,而且通用枚举数 例如 read、write、append等 使得用户几乎不可能编译他们的现存代码。一个解决方案是破坏这些名称,例如 io_read 、 io_write等等。另一个解决方案是修改语言来添加枚举的范围,但是在当时是不可能实现的。(一个折衷的方案是将枚举封装在类或类层次结构中,这时枚举的标记名称及其枚举数填充封闭类范围。)换句话说,将枚举放在类中的动机 至少是原始动机 不是理论上的,而是全局命名空间污染问题的一个实际解决方案。
对于 V2CLI 枚举,将枚举封装在类中不再有任何明显的好处。实际上,如果您看看 System命名空间,您就会看到枚举、类和接口都在同一个声明空间中存在。
4.2 隐式装箱OK,我们食言了。在政治领域中,这会使我们输掉一场选举。在语言设计中,这意味着我们在实际经验中强加了一个理论的位置,而且实际上它是一个错误。一个类似的情形是,在原始多继承语言设计中,Stroustrup 认为在派生类的构造函数中无法初始化一个虚基类子对象,这样,C++ 语言要求任何作为虚基类的类都必须定义一个默认构造函数。这样只有默认的构造函数才会被后续的虚派生调用。
虚基类层次结构的问题是将初始化共享虚子对象的职责转推到每个后续的派生类中。举例来说,我定义了一个基类,它的初始化需要分配一个缓冲区,用户指定的缓冲区大小作为构造函数的一个参数传递。如果我提供了两个后续的虚继承,名为 inputb和 outputb,每个都需要提供基类构造函数的一个特定值。现在我从 inputb和 outputb派生一个 in_out类,那么两个共享虚基类子对象的值都没有明显地被求值。
因此,在原版语言设计中,Stroustrup 在派生类构造函数的成员初始化列表中,禁用了虚基类的显式初始化。虽然这解决了问题,但是实际上无法控制虚基类的初始化证明是不可行的。国家健康协会的 Keith Gorlen(他实现了一个名为 nihcl的免费版本 SmallTalk集合库)劝告 Bjarne,让他必须考虑一个更加灵活的语言设计。
一个面向对象的层次设计原则是一个派生类只应该涉及其本身和直接基类的非私有成员。为了支持一个灵活的虚继承初始化设计,Bjarne 不得不破坏了这个原则。层次中最底层的类负责初始化所有虚子对象,不管他们在层次结构中有多深。例如,inputb和 outputb都有责任显式初始化他们的直虚基类。在从 inputb和 outputb派生 in_out类时,in_out开始负责初始化一度被移除的虚基类,并且 inputb和 outputb中的显式初始化被抑制了。
这提供了语言开发人员所需要的灵活性,但是却以复杂的语义为代价。如果我们将虚基类限定为无状态,并且只允许指定一个接口,那么就消除了这种复杂性。这在 C++中是一个推荐的设计方案。在 C++/CLI中,这是 Interface类型的方针。
以下是一个代码实例,完成一些简单的功能 在本例中,显式装箱很大程度上是无用的语法负担。
// 原版语言设计需要显式 __box 操作 int my1DIntArray __gc[] = { 1, 2, 3, 4, 5 };Object* myObjArray __gc[] = { __box(26), __box(27), __box(28), __box(29), __box(30) };Console::WriteLine( "{0}t{1}t{2}", __box(0), __box(my1DIntArray-GetLowerBound(0)), __box(my1DIntArray-GetUpperBound(0)) );
您可以了解,后面会有许多装箱操作。在 V2中,值类型的装箱是隐式的:
// 修订版语言进行隐式装箱 arrayint^ my1DIntArray = {1,2,3,4,5}; arrayObject^^ myObjArray = {26,27,28,29,30};Console::WriteLine( "{0}t{1}t{2}", 0, my1DIntArray-GetLowerBound( 0 ), my1DIntArray-GetUpperBound( 0 ) );4.3 装箱值的跟踪句柄
装箱是 CLI 统一类型系统的一个特性。值类型直接包含其状态,而引用类型有双重含义:命名实体是一个句柄,这个句柄指向托管堆上分配的一个非命名对象。举例来说,任何从值类型到对象的初始化或者赋值,都需要值类型放在 CLI 堆中(图像装箱发生的位置)首先分配相关的内存,然后复制值类型的状态,最后返回这个匿名值 / 引用的组合。因此,用 C# 编写如下代码时,
object o = 1024; // C# 隐式装箱
代码的简洁使得装箱十分接近透明。C# 的设计不仅隐藏了后台所发生的操作的复杂性,而且也隐藏了装箱本身的抽象性。另一方面,V1考虑到它可能导致效率降低,所以直接要求用户显式编写指令:
Object *o = __box( 1024 ); // V1 显式装箱
就像在本例中还有其他选择一样。依我之见,在这种情况下强迫用户进行显式请求就像一个人的老妈在他出门时不断唠叨一样。现在我们会照顾自己了,难道你不会?一方面,基于某些原因,一个人应该学会内敛,这被称为成熟。另一方面,基于某些原因,一个人必须信任子女的成熟。把老妈换成语言的设计者,程序员换成子女,这就是 V2中装箱成为隐式的原因。
Object ^o = 1024; // V2 隐式装箱
__box关键字在原版语言设计中是第二重要的服务,这种设计在C#和 Microsoft Visual Basic .NET 语言中是没有的:它提供词汇表和跟踪句柄来直接操作一个托管堆上的装箱实例。例如,考虑如下小程序:
int main() { double result = 3.14159; __box double * by = __box( result ); result = 2.7; *br = 2.17;Object * o = br; Console::WriteLine( S"result :: {0}", result.ToString() ) ; Console::WriteLine( S"result :: {0}", __box(result) ) ; Console::WriteLine( S"result :: {0}", br ); }
WriteLine的三个调用生成的底层代码显示了访问装箱值类型值的不同代价(感谢Yves Dolce指出这些差异),这里黑体的行显示了与每个调用相关的开销。
// Console::WriteLine( S"result :: {0}", result.ToString() ) ; ldstr "result :: {0}" ldloca.s&n
来源:http://www.tulaoshi.com/n/20160219/1616834.html
看过《将程序从托管扩展 C++ 迁移到 C++/CLI(1)》的人还看了以下文章 更多>>