您好、欢迎来到现金彩票网!
当前位置:老k棋牌 > 栈上托 >

转换指南: 将程序从托管C++扩展迁移到C++CLI 学步园

发布时间:2019-08-09 21:52 来源:未知 编辑:admin

  语言标准的动态范型扩展。本文列举了V1版本语言的特色 ,以及它们在V2版本中的对应(如果存在);并指出为不存在相应对应的V1特色构建的语言特性。(68打印页)

  C++/CLI代表ISO-C++标准语言的一个动态编程范型扩展(dynamic programming paradigm extension). 在原版语言设计(V1)中有许多显著的弱点,我们觉得在修订版语言设计(V2)中已经修正了。 本文列举了V1版本语言的特色和它们在V2版本中的对应 (如果有这样的对应存在的话);并指出为对应不存在的V1特色构建的语言特性。对于有兴趣的读者,附录中提供新语言设计的扩展原理。 另外,一个源代码级别的转换工具(mscfront)正在开发中,而且可能在C++/CLI的发布版中提供给希望 自动化移植V1代码到新语言设计的人。

  本文分为五个章节加一个附录。第一节讨论主要的语言关键字,特别是双下划线的移除以及上下文性和分段关键字。第二节着眼于托管数据类型的变化——特别是托管引用类型和数组类型。还可以在这里找到有关确定性终结化语义 (deterministic finalization)的详细讨论。关于类成员的变化,例如属性、索引属性和操作符,是第三节的焦点。第四节着眼于托管枚举、内部和约束指针的语法变化。它也讨论了许多可观的语义变化,例如隐式装箱的引入、托管CLI枚举的变化,和对值类默认构造函数的支持的移除。第五节有几分像大杂烩——声名狼藉的杂项。可以在里面找到对类型转换符号、字符串常数的行为和参数数组的讨论。

  原版到修订版的一个大体转换是在所有关键字中去掉双下划线。举例来说,一个属性现在被声明为property而不是__property。在原版设计中使用双下划线前缀的两个主要原因是:

  这样的话,为什么我们移除双下划线(并且引入了一些新的标记)?不是的,这不代表我们不再考虑和标准保持一致!

  我们继续致力于和标准一致。尽管如此,我们意识到对CLI动态对象模型的支持表现出了一种全新的强大的编程范型。我们在原版设计以及设计与发展C++语言的经验使我们确信,对这个新范型的支持需要它自己的高级关键字和标记。我们想提供一个新范型的一流表达方式,整合它并且支持标准语言。我们希望你会感到修订版语言设计提供了对这两种截然不同的对象模型的一流的编程体验。

  类似的,我们很关心最小化这些新的关键字的对现有代码可能造成的冲击。这是用上下文性和分段关键字解决的。在我们着眼于实际语言语法的修订之前,让我们试试搞清楚这两个特别的关键字的风味。

  一个上下文性关键字在特定的语言环境中有特殊的含义。例如,在通常的程序中,sealed被识别为一个普通标识符。但是,在一个托管引用类类型的声明部分,它被识别为类声明上下文中的一个关键字。这最小化了在语言中引入一个新的关键字的潜在影响,我们觉得这对我们的有旧代码基础的用户非常重要。同时,它允许新的功能的使用者获得一流的新增语言特色的体验——我们在觉得原版设计中缺少这些因素。我们将在2.1.2节中看到sealed的用法。

  一个分段关键字是上下文性关键字的特例。字面上是一个上下文性修饰符和一个现存关键字配对,用空格分隔。这个配对被识别为一个语法单位,例如value class(示例参见2.1),而不是两个单独的关键字。基于现实的因素,这意味着一个重新定义value的宏,如下所示:

  不会在一个类声明中去掉value。如果确实要这么做的话,不得不重新定义语法单位对,如下所述:

  考虑到现实的因素,这是十分必要的。否则,现存的#define可能转换分段关键字的上下文性关键字部分。(译者注:例如2003年1月份的平台SDK头文件中的#define interface struct,参见拙作)。

  声明托管数据类型和创建以及使用这些类型的对象的语法已经被大加修改,以提高对ISO-C++类型系统的兼容性。这些更改在后面的小节中详述。委托的讨论延后到3.3节以用类的事件成员表述它们——这是第3节的主题。(关于更加详细的跟踪引用语法介绍内幕和设计上的主要转变的讨论,参见附录 推动修订版语言设计。)

  在原版语言定义中,一个引用类类型以__gc关键字开头。在修订版语言中,__gc关键字被两个分段关键字ref class或者ref struct之一替代。struct或者class的选择只是指明在类型体中开头未明确访问级别的部分的默认公开(对于struct)或者私有(对于class)默认访问级别。

  类似地,在原版语言设计中,一个引用类类型以__value关键字开头。在修订版语言中,__value关键字被两个分段关键字value class或者value struct之一代替。

  在原版语言设计中,一个接口类型,是用关键字__interface指明的。在修订版语言中,它被interface class替代。

  选择ref(对于引用类型)而不是gc(对于垃圾收集的)的想法是更好地暗示这个类型的本质。

  在原版语言定义中,关键字__abstract可以被放在类型关键字之前(__gc之前或者之后)以指明该类尚未完成而且此类的对象不能在程序中创建:

  在修订版语言设计中,abstract上下文性关键字被限定在类名之后,类体、基类派生列表或者分号之前。

  在原版语言定义中,关键字__sealed被放在类关键字之前(__gc之前或者之后)以指明类不能被继承:

  在V2语言设计中,sealed上下文性关键字限定在类名之后,类体、基类派生列表或者分号之前(你可以在声明一个继承类的同时封闭它。举例来说,String类隐式派生自Object)。封闭一个类的好处是允许静态(就是说,在编译时)解析这个密封引用类型的对象的所有的虚函数调用。这是因为密封指示符保证了String跟踪句柄不能指向一个可能重载被触发的虚方法的派生类对象。

  也可以将一个类既声明为抽象类也声明为封闭类。这是一种被称为静态类的特殊情况。这在CLI文档中描述如下

  同时为抽象和封闭的类型只能有静态成员,并且以一些语言中调用命名空间一样的方式服务。

  在CLI对象模型中,只支持公有方式的单继承。但是,在原始语言定义中仍然保留了ISO-C++对基类的解释,没有访问关键字的基类将默认成为私有派生类型。这意味着每一个CLI继承声明不得不用一个public关键字来代替默认的解释。很多用户认为编译器似乎过于严谨。

  在修订版语言定义中,CLI继承定义缺少访问关键字时,默认是以公有的方式派生。这样,公有访问关键字就不再必要,而是可选的。虽然这个改变不需要对V1的代码做任何的修改,出于完整性考虑我仍将这个变化列出。

  在原版语言定义中,一个引用类类型对象是使用ISO-C++指针语法声明的,在星号左边使用可选的__gc关键字。例如,这是V1语法下多种引用类类型对象的声明:

  在修订版语言设计中,引用类类型的对象用一个新的声明性符号(^)声明,正式的表述为跟踪句柄,更不正式的表述为帽子。(跟踪这个形容词强调了引用类型对象位于CLI堆中,因此可以透明地在堆的垃圾收集压缩过程中移动它的位置。一个跟踪句柄在运行时被透明地更新。两个类似的概念:(a)追踪引用(%)和(b)内部指针(interior_ptr),在第4.4.3节讨论。

  对一个跟踪句柄使用__gc修饰符是不必要的而且是不被支持的。对象本身的用法并未变化,它仍旧通过指针成员选择操作符(-)访问成员。例如,这是上面的V1文字翻译到修订版语言语法的结果:

  在原版语言设计中,现有的在传统堆和托管堆上分配的两种new表达式很大程度上是透明的。在几乎所有的情况下,编译器能够从上下文正确决定所需的是传统堆还是托管堆。例如:

  在上下文性堆分配的结果并非所期望的行为时,可以用__gc或者__nogc关键字指引编译器。在修订版语言中,使用新引入的gcnew关键字来明显化两个new表达式的不同本质。例如,上面三个声明在修订版语言中看起来像这样:

  (在第4节中讨论interior_ptr的更多细节。通常,它表示一个对象的地址,这个对象不必位于托管堆上。如果指向的对象确实位于托管对象堆,那么它在对象被重新定位时被透明地更新)

  这是用修订版语法重写的同样的初始化过程,注意引用类型是一个gcnew表达式的结果时不需要“帽子”。

  在新的语言设计中,0不再表示一个空地址,而是被处理为一个整型,和1、10、100一样,这样我们需要引入一个特殊的标记来代表一个空值的跟踪引用。例如,在原版语言设计中,我们如下初始化一个引用类型为一个空的对象引用:

  在修订版语言中,任何从值类型到一个Object的初始化或者赋值都导致一个值类型的隐式装箱(implicit boxing)。在修订版语言中,obj和obj2都被初始化为装箱过的Int32对象,分别具有值0和1。例如

  因此,为了允许显式的初始化、赋值一个跟踪句柄为空,以及和空指针比较,我们引入了一个新的关键字,nullptr。这样V1示例的正确的修订版看起来如下:

  这使得从现存V1代码到修订版语言设计的移植稍微复杂一点。例如,考虑如下值类声明:

  这里args和env都是CLI引用类型。在构造函数中将他们初始化为0的语句在转移到新的语法的过程中必须修改为nullptr:

  类似的,把这些成员和0比较的测试也必须改为和nullptr比较。这是原始语法:

  而这里是修订版。转换每个0的实例到nullptr(翻译工具对这个转换有所帮助,自动处理很多——如果不是全部——的实例,包括使用NULL宏的地方。

  nullptr可以转化成任何跟踪句柄类型或者指针,但是不能提升为一个整型类型。例如,在如下初始化集合中,nullptr只在开头两个初值中有效

  是有歧义的,因为nullptr既匹配一个跟踪句柄也匹配一个指针,而且在两者中没有一个优先选择(这需要一个显式的类型强制转换来消除歧义)。

  由于0是整型。当没有f(int)的时候,它会通过一个标准转换无歧义地匹配f(char*)。在没有精确匹配时,标准转换被给与了对于值类型的隐式装箱的优先权。这是这里没有歧义的原因。

  原版语言设计中的CLI数组的声明是标准数组声明的有点不直观的扩展,一个__gc关键字放在数组对象名和可能的逗号填充的维数之间,如下一对示例所示:

  这在修订版语言设计中被简化了,我们使用一个类似于模板的,模仿STL的向量声明。第一个参数指定元素类型。第二个参数指定数组维数(默认值是1,所以只有多维数组才需要第二个参数)。数组对象本身是一个跟踪句柄,所以必须给它一个帽子。如果元素类型也是一个引用类型,那么,它们也必须被标记。例如,上面的示例,在修订版语言中表达的时候看起来像这样:

  因为引用类型是一个跟踪句柄而不是一个对象,所以可能将一个CLI数组类型用于函数的返回值类型(传统数组不能用于函数返回值)。在原版语言中,其语法也有点不直观。例如:

  在V2中被大大简化了(注意因为修订版语言设计中的装箱是隐式的,__box操作符被去掉了——关于其讨论参见第4节。

  因为数组是一个CLI引用类型,每个数组对象的声明都是一个跟踪句柄。因此,它必须在CLI堆上被分配(简洁初始化隐藏了在托管堆上进行分配的细节)。这是原版语言设计中一个数组对象的显式初始化形式:

  回忆一下,在新的语言设计中,new表达式被gcnew替代了。数组的维大小作为参数传递给gcnew表达式如下:

  在修订版语言中,gcnew后面可以跟一个显式的初始化列表,这在V1语言中不 被支持,例如:

  在原版语言定义中,类的析构函数允许存在于引用类中,但是不允许存在于值类中。这在V2语言设计中没有变化,但是,析构函数的语义有可观的变化。怎样和为什么变化(以及这会对现存V1代码的翻译造成怎样的影响)是本节的主题。这可能是本文中最复杂的一节,所以我们慢慢来讲。这也可能是两个语言版本之间最重要的编程级别的修改,所以值得以循序渐进的方式来编排资料。

  在对象关联的内存被垃圾回收机收回之前,如果对象有一个相关的Finalize()方法存在,那么它将被调用。你可以把它想象为一个超级析构函数,因为它和对象编程生命期无关。我们称此为终结化。何时甚至是否调用Finalize()方法是不确定的。这就是我们提到垃圾收集代表不确定的终结化(non-deterministic finalization)时表达的意思。

  不确定的终结化和动态内存管理合作的很好。当可用内存缺少到一定程度的时候,垃圾收集机介入,并且很好地工作。在内存收集环境中,用析构函数来释放内存是不必要的。你第一次开始写这种程序的时候不为潜在的内存泄漏发愁才怪,但是容易就会适应这种机制了。

  然而,不确定的终结化机制在对象维护一个关键的资源,例如一个数据库连接或者某种类型的锁的时候运转并不好。这种情况下我们需要尽快释放资源。在传统代码的环境下,这个是用构造/析构组合解决的。不管是通过执行完毕声明对象的本机代码块还是通过抛出异常造成的拆栈,对象的生命期一终止,析构函数就介入并且自动释放资源。这个机制运转得很好,而且在原版语言设计中没有它的存在是一个很大的失误。

  CLI提供的解决方案是实现IDisposable接口的Dispose()方法的类。这里的问题是Dispose()方法需要用户显式地调用。这是个错误的倾向而且因此是个倒退。C#语言提供一个适度的自动化方式,使用一个特别的using语句。我们的原版语言设计——我已经提到过——根本没有提供特别的支持。

  两个析构函数都被重命名为Finalize()。B的Finalize()在WriteLine()之后加入一个A的Finalize()的调用。这些就是垃圾收集机在终结化过程中默认调用的代码。它的内部转换结果看起来可能像这样:

  这个总结出来的析构函数里面是些什么东西呢?是两个语句。一个是调用GC::SuppressFinalize()以确保免除对这个对象的Finalize()方法的后续调用。另一个是实际上的Finalize()调用。回忆一下,这表达了用户提供的这个类的析构函数。它看起来可能像这样:

  这个实现允许用户立刻显式触发类的Finalize()方法而不是由垃圾收集机随时调用,它并不真的依赖使用Dispose()方法的方案。这在修订版语言设计中被更改了。

  在修订版语言设计中,析构函数被内部重命名为Dispose(),并且引用类自动扩展以实现IDisposable接口。换句话说,我们的这对类被如下转换:

  当析构函数被调用的时候,无论是通过在V2下显式调用,还是通过对跟踪句柄应用delete,底层的Dispose()方法都会自动被调用。如果这是一个派生类,一个对基类的Dispose()方法的调用会被插入到总结出的方法的末尾。

  但是这样也没有给我们确定性终结化的方法。为了解决这个问题,我们需要局部引用对象的额外支持(在原版语言设计中没有类似的支持,所以没有翻译的问题)。

  修订版语言支持一个局部栈上的对象或者一个可直接访问的类成员的声明(注意这在Microsoft Visual Studio 2005的Beta1发布版中不可用)。析构函数和在2.4.3节中描述的Dispose()方法结合的时候,结果就是引用类型的终结语句的自动调用。使CLI社区苦恼的非确定性终结化这条暴龙终于被驯服了,至少对于C++/CLI的用户来说是这样。让我们来查看和了解这意味着什么。

  首先,我们这样定义我们的一个引用类,使得对象创建代码在类构造函数中获取一个资源。其次,在类的析构函数中。我们释放对象创建时获得的资源

  对象声明为局部的,使用没有附加帽子的类型名。所有对对象的使用,比如调用成员函数,是通过成员选择点(.)而不是箭头(-).在块的末尾,转换成Dispose()的相关的析构函数,被自动调用。

  相对于C#中的using语句来说,这只是语法上的点缀而不是对CLI根本约定——所有引用类型的对象必须在CLI堆上分配——的挑战。基础语法仍未变化。用户可能已经等价地编写了下面的语句(这很像编译器执行的内部转换):

  在修订版语言设计中,析构函数再次和构造函数配对成为和一个局部对象生命期关联的获得/释放资源的有效的机制。这是一个显著的和非常令人震惊的成就,并且语言设计者应该因此被大力赞扬。

  在修订版语言设计中,像我们已经看到的那样,构造函数被综合成Dispose()方法。这意味着在析构函数没有被显式调用的情况下,垃圾收集机,在终结化的时候,不会像以前那样为对象查找相关的Finalize()方法。为了同时支持析构函数和终结化,修订版语言引入了一个特殊的语法来提供一个终结化。举例来说:

  !前缀打算暗示同样引入析构函数而使用的符号(~),也就是说,两种生命期末的方法的名字都是在类名称前面加一个符号前缀。如果派生类中有一个总结的Finalize()方法,那么在其末尾会加入一个基类的Finalize()方法的调用。如果析构函数被显式地调用,那么终结化会被抑制。这个转换看起来可能像这样:

  这意味着,只要一个引用类包含一个特别的析构函数,一个V1程序在V2编译器下的运行期行为被暗地修改了。需要的翻译算法看起来如下:

  从V1移植你的代码到V2的过程中可能漏掉这个转换。如果引用程序某种程度上依赖于相关终结化方法的执行,那么应用程序的行为将被暗地修改。

  属性和操作符的声明在修订版语言设计中已经被大范围重写了,隐藏了原版设计中暴露的底层事件细节。另外,事件声明也被修改了。

  V2中V1不支持的修订之一,静态构造函数现在可以在类外部定义了(在V1中它们必须被定义为内联的),并且引入了一个委托构造函数的符号。

  在原版语言设计中,每一个set或者get属性存取方法都被规定为一个独立的成员函数。每个方法的声明都有__property关键字作为前缀。方法名以set_或者get_开头,后面接属性的实际名称(用户看见的那个).这样,一个获得向量的x坐标的属性存取方法将命名为get_x,用户将以名称x来访问它。这个名称转换和单独的方法规定实际上反映了属性的运行时刻的底层实现。例如,这是我们的向量,有一些坐标属性:

  这被发现是让人迷惑的,因为属性相关的函数被展开了,并且需要用户从语法上统一相关的set和get。而且它在语法上过于冗长,并且感觉上不甚优雅。在修订版语言设计中,这个声明更类似于C#——property关键字后面接属性的类型以及属性的原名。set存取和get存取方法被放在属性名之后的一段中。注意不像C#那样,存取的方法的符号被指出来了。例如,这里是上面的代码在新语言设计下的翻译:

  如果两个属性的存取方法表现为不同的访问级别——例如一个公有的get和一个私有的或者保护的set,那么可以指定一个显式的访问标志。默认情况下,属性的访问级别就是围绕它的访问级别。例如,在上面的Vector定义中,get和set方法都是公有的。为了让set方法成为保护或者私有的,必须如下修改定义

  属性中的访问关键字的作用域延伸到属性的结束或者另一个访问关键字的声明。它不会延伸到属性的定义之外。例如在上面的声明中,Vector::dot()是一个公有成员函数。

  为三个Vector坐标编写set/get属性有点乏味,因为本质是定死的:(a)用适当类型声明一个私有状态成员,(b)在用户希望取得它的值的时候返回,以及(c)设置它为用户希望赋的任何值。在修订版语言设计中,一个简洁属性语法可以用于自动化这个使用方式:

  简洁属性语法的有趣的副作用是,在编译器自动生成后台状态成员时,除非通过set/get访问函数,否则这个成员在类的内部不可访问 。这就是所谓的严格限制的数据隐藏!

  原版语言对索引属性的支持的两个主要缺点是不能提供类级别的下标,就是说,所有索引属性必须有一个名字。举例来说,这样就没有办法提供可以直接应用到一个Vector或者Matrix类的对象的托管下标操作符。其次,一个次要的缺点是很难区分属性和索引属性——参数的数目是唯一的判断方法。最后,索引属性有和非索引属性同样的问题——存取函数没有被识别为一个基本单位,而是分为单独的方法。举例来说:

  如你所见,只能用额外的参数来指定一个二维或者单维的索引。在修订版语法中,索引以名字后面的方括号([,])和标志每个索引的数目和类型的参数为特征:

  在修订版语法中,为了指定一个可以直接应用于类的对象的类级别索,default关键字又被用于替换一个 显式的名称。例如:

  在修订版语法中,当指定了default索引属性的时候,下面两个名字被保留:get_Item和set_Item。这是因为它们是default索引属性产生的底层名称。

  声明一个委托和普通事件的仅有的变化是移除了双下划线,如下面的示例所述。在去掉了之后,这个更改被认为是完全没有争议的。换句话说,没有人支持保持双下划线,每个人现在看来都同意双下划线使得语言感觉很难看。

  事件(以及委托)是引用类型,这在V2中更为明显,因为有帽子(^)的存在。除了普通形式之外,事件支持一个显式的声明语法,用户在显式的场合指定事件关联的add()、raise()、和remove()方法。(只有add()和remove()方法是必须的;raise()方法是可选的)。

  在V1设计中,如果用户选择实现这些方法,即使她必须决定这个还不存在的事件的名字,她也实现一个显式的事件声明。单独的方法以add_EventName、raise_EventName、和remove_EventName的格式识别,如下面的引用自V1语言规范的示例所述:

  这个设计的问题主要是感官上的,而不是功能上的。虽然语言设计支持添加这些方法,但是上面的示例看起来并不是一目了然。因为V1属性和索引属性的存在,类声明中的方法看起来千疮百孔。稍微更令人沮丧的是缺少一个实际上的E1事件声明。(底层实现细节再一次地在用户级别的语法特性暴露了,显然增加了语法的复杂性。)这只是劳而无功。V2设计大大简化了这个声明,如下面的译文所述。在事件声明及其相关委托类型之后的一对花括号中指定两个或者三个方法如下:

  虽然在语言设计方面,人们因为语法的简单枯燥而倾向忽视它,但是如果对语言的用户体验有很大的潜移默化的影响,那么它实际上很有意义。一个令人迷惑的不优雅的语法增加开发进程的风险,很大程度上就像一个脏的或者不清晰的挡风玻璃增加开车的风险一样。在修订版语言设计中,我们努力使语法像一块高度磨光的新安装的挡风玻璃一样透明。

  __sealed关键字在V1版中被用于修饰一个引用类型,禁止从此继续派生——像我们在2.1.2节看到的那样——或者修饰一个虚函数,禁止从此继续重载。举例来说:

  在新的语言设计中,sealed是在名字之后而不是像V1中的那样,允许在实际函数原型之前任何地点。另外,sealed的使用也需要同时使用一个显式的virtual关键字。换句话说,上面的derived的正确译文如下所述:

  缺少virtual关键字的话会产生一个错误。在V2中,上下文关键字abstract可以在=0的地方用来指明一个纯虚函数。这在V1中不被支持。举例来说:

  可能原版语言设计最惊人的方面是它对于操作符重载的支持——或者更加适当地说,是明显的缺乏支持。举例来说,在一个引用类型的声明中,不是内建的operator+语法,而是显式编写操作符的底层内部名称——在这个例子中是op_Addition。但是,更加麻烦的是,操作符的调用必须用这个名称来显式触发的问题,这样就妨碍了操作符重载的两个主要好处:(a)直观的语法,和(b)混合现有类型和新的类型的能力。举例来说:

  在语言的修订中,恢复了传统C++程序员的普通期望,声明和使用内建的操作符。这里是翻译成V2语法的Vector类:

  谈到令人不愉快的感觉,在V1语言设计中不得不写op_Implicit函数来指定一个转换感觉上就不像C++。例如,这是引自V1语言规范的MyDouble类定义:

  这就是说,给定一个整数,强制转换这个整数成为MyDouble的算法是通过op_Implicit操作符实现的。进一步说,这个转换将被编译器隐式执行。类似的,给定一个MyDouble对象,两个op_Explicit操作符分别是强制转换对象到一个整型或者一个托管的字符串实体的算法。但是,编译器不会执行这个转换,除非用户显式要求。

  如果不是每个成员都有的显式公有访问标志看起来很古怪的话,C#代码看起来比C++的托管扩展更加像C++。所以我们不得不修复这个问题。但是我们怎么才能做到?

  一方面,C++程序员把表达式里面的单参数构造函数的省略掉而翻译为一个转换操作符。但是,另一方面,这个设计被证明是如此难于处理,以致ISO-C++委员会引入了一个关键字explicit,只是为了处理它的意外后果—例如,一个有一个整型变量为维数参数的Array类将隐式地转换任何整型变量到一个Array对象,甚至在这是用户最不需要的情况的时候也这样。AndyKoenig是第一个引起我注意这个问题的人,那时他解释了一个设计习惯,构造函数中的挂名第二参数只是用来阻止这种不好的事情的发生。所以我不会对C++/CLI中的缺乏单参数构造函数的隐式转换感到遗憾。

  另一方面,在C++中设计一个类的时候实现一个转换对从来不是一个好主意。关于这个的最好的示例是标准string类。隐式转换是有一个C风格的字符串的单参数构造函数。但是,它没有提供一个对应的隐式转换操作符来转换一个string对象到一个C风格的字符串——而是需要用户显式调用一个命名函数——在这个示例中,是c_str()。

  这样,转换操作符的隐式/显式的行为的关联(以及封装这样的转换的集合到统一的声明形式)看起来是一个C++对转换操作符的原始支持的改进,这个支持自从1988年Robert Murray发布了关于UsenixC++的标题为Building Well-Behaved Type Relationships in C++的讲话之后,已经成为一个公开的警世篇,讲话最终导致explicit关键字。修订版V2语言对转换操作符的支持看起来象下面这样,比C#的稍微简略一点,因为操作符的默认行为支持隐式转换算法的应用:

  V1到V2的另一个变化是,V2中的单参数构造函数以声明为explicit的方式处理。这意味着为了触发它的调用,需要一个显式的转换。但是要注意,如果一个显式的操作符已经定义,那么是它而不是单参数构造函数会被调用。

  经常有必要在实现接口的类中提供两个接口成员的实例——一个用于通过接口句柄操作类对象,另一个用于通过类界面操作对象。例如:

  在V1中,我们通过提供一个用接口名和类名指名的显式接口成员声明来解决这个问题。 类界面操作的方法没有指名接口名。在这个示例中,当通过一个R的实例显式调用Clone()时,这样可以免除对其返回值的类型向下强制转换。

  在V2中,一个通用重载机制被引入,用来替换前面的语法。我们的示例会被如下重写:

  这个修订需要给出重载接口成员的方法一个在类中唯一的名称。这里我提供了一个相对笨拙的名称InterfaceClone()。修订版的行为仍旧是一样的——通过ICloneable接口的调用触发我们重命名的InterfaceClone(),而通过R类型的对象调用的是第二个Clone()实例。

  在V1中,虚函数的访问级别并不影响它在派生类中是否可以被重载。这在V2中被修改了。在V2中,不能重载基类中不可访问的虚函数。例如:

  对于这个设计来说,实际上没有在V2中的对应。要重载这个函数,必须简单地把基类的成员改成可以访问的——也就是说,非私有的。继承的方法不必沿用同样的访问级别。在这个示例中,最小的改变是把My的成员的声明为保护访问等级的。通过My来访问这个方法仍旧是被禁止的。

  注意在V2下,如果基类缺少一个显式virtual关键字,那么会产生一个警告。

  虽然static const整合成员仍旧被支持,但是它们的连接方式属性被修改了。以前的连接方式属性现在通过一个literal整合成员来完成。例如,考虑如下V1类:

  为了具有同样的中间语言的literal属性,声明应该改为使用新支持的literal数据成员,如下所示:

  本节中我们着眼于CLI枚举类型和值类类型,同时审视装箱和对CLI堆上的装箱示例的访问,以及考虑内部和约束指针。这个领域的语言变化很大。

  原版语言的CLI枚举声明的前面有一个__value关键字。这里的意图是区分传统枚举和派生自System::ValueType的CLI枚举,同时暗示它们具有同样的功能。例如,

  修订版语言用强调后者的类本质而不是它的值类型本源的方法来解决这个区分传统枚举和CLI枚举的问题。同样地,__value关键字被废弃了,替换成了一对分段关键字enum class。这实现了引用类、值类和接口类的声明中的关键字对的押韵。(译者注:写程序居然要押韵?^_^bb)

  对于传统C++程序员,这个问题的自然的答案是,被调用的f()的重载实例是f(int)。枚举是一个整型符号常量,并且在此示例中作为标准整型被转换。实际上,在原版语言设计中,这本质上就是这个调用解析的结果。这导致一些惊奇——不是在我们以传统C++框架思想使用它的时候,而是在我们需要它们和现存的BCL(基类库)互动的时候,这里枚举是一个间接派生自Object的类。在修订版语言设计中,被调用的f()实例是f(Object^)。

  V2选择强制不支持CLI枚举和算术类型之间的隐式转换。这意味着任何从托管枚举对象到算术类型的赋值都需要一个显式的强制转换。举例来说,假定

http://drpetermitoff.com/zhanshangtuo/118.html
锟斤拷锟斤拷锟斤拷QQ微锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷微锟斤拷
关于我们|联系我们|版权声明|网站地图|
Copyright © 2002-2019 现金彩票 版权所有