对象类型转换:dynamic_cast 和 pointer void *

对象类型具有特定的转换规则,这些规则在源和目标变量类型不匹配时适用。内置类型的规则已经在第 2.6 章 类型转换中讨论过。复制时结构体类型转换的细节在 结构体布局和继承 一节中进行介绍。

对于结构体和类,是否允许类型转换主要取决于它们是否应在继承链上存在关联。继承体系中属于不同分支的类型或根本不相关的类型不能相互转换。

对象(值)和指针的类型转换规则不同。
 

对象

如果类型 B 拥有一个接受类型 A 参数的构造函数(有不同变体,比如按值、按引用或按指针,但通常形式为 B(const A &a)),则类型 A 的对象可以赋值给类型 B 的对象。这样的构造函数也称为转换构造函数。

在没有显式转换构造函数的情况下,编译器会尝试使用隐式拷贝赋值运算符,即 B::operator=(const B &b)。要使这种隐式转换(从 A 到 B)有效,类 A 和 B 必须在同一继承链中。如果 A 继承自 B(包括间接继承),则在拷贝到 B 时,A 中新增的属性将会消失。如果 B 继承自 A,则只会将 A 中存在的属性部分拷贝到 B。通常不推荐这种隐式转换。

此外,编译器也不一定提供隐式拷贝运算符。特别是,如果类具有带 const 修饰符的字段,则认为禁止拷贝(详见后文)。

ShapesCasting.mq5 脚本中,我们使用形状类层次结构来演示对象类型转换。在 Shape 类中,type 字段被特意设为常量,因此尝试将 Square 对象转换(赋值)为 Rectangle 对象会导致编译器错误,并给出详细解释:

   attempting to reference deleted function 'void Rectangle::operator=(const Rectangle&)'
      function 'void Rectangle::operator=(const Rectangle&)' was implicitly deleted
      because it invokes deleted function 'void Shape::operator=(const Shape&)'
   function 'void Shape::operator=(const Shape&)' was implicitly deleted
      because member 'type' has 'const' modifier

根据此消息可以看出,Rectangle::operator=(const Rectangle&) 拷贝方法已被编译器隐式移除(编译器提供了默认实现),因为它在 Shape::operator =(const Shape&) 基类中使用了类似的方法,而后者又因存在带有 const 修饰符的 type 字段而被移除。此类字段只能在创建对象时设置,而编译器不知道如何在这种限制下拷贝对象。

顺便说一句,“删除”方法的效果不仅对编译器有效,对程序员也有效:更多相关内容将在 继承控制:final 和 delete 一节中讨论。

可通过以下方法解决此问题:移除 const 修饰符,或自行提供各自的赋值运算符实现方式(其中不涉及 const 字段,并将内容与"Rectangle" 类型的说明一起保存):

   Rectangle *operator=(const Rectangle &r)
   {
      coordinates.x = r.coordinates.x;
      coordinates.y = r.coordinates.y;
      backgroundColor = r.backgroundColor;
      dx = r.dx;
      dy = r.dy;
      return &this;
   }

请注意,此定义返回指向当前对象的指针,而编译器生成的默认实现为 void 类型(如错误消息中所示)。这意味着,编译器提供的默认赋值运算符不能在 x = y = z 链中使用。如果需要此功能,请显式重写 operator= 并返回所需的非 void 类型。

 

指针

最大的作用是将指针转换为不同类型的对象。

理论上,用于转换对象类型指针的所有选项可以简化为三种:

  • 从基类到派生类的转换,这属于向下类型转换(向下转换),因为通常用倒置树来绘制类层次结构;
  • 从派生类到基类的转换,这属于向上类型转换(向上转换);
  • 在层次结构不同分支的类之间的转换,甚至不同家族的类之间的转换。

最后一种选项被禁止(我们会收到编译错误)。编译器允许前两种,但如果“向上转换”是自然且安全的,那么“向下转换”可能会导致运行时错误。

void OnStart()
{
   Rectangle *r = addRandomShape(Shape::SHAPES::RECTANGLE);
   Square *s = addRandomShape(Shape::SHAPES::SQUARE);
   Circle *c = NULL;
   Shape *p;
   Rectangle *r2;
   
   // OK
   p = c;   // Circle -> Shape
   p = s;   // Square -> Shape
   p = r;   // Rectangle -> Shape
   r2 = p;  // Shape -> Rectangle
   ...
};

当然,当使用指向基类对象的指针时,即使相应的对象位于指针处,也不能对其调用派生类的方法和属性。否则我们会遇到编译错误“未声明的标识符”。

但是,指针支持 显式强制转换 语法(参见 C 风格),该语法允许在表达式中将指针“动态”转换为所需类型,并对其进行取消引用,而无需创建中间变量。

Base *b;
Derived d;
b = &d;
((Derived *)b).derivedMethod();

这里,我们创建了一个派生类对象 (Derived) 和一个指向它的基类指针 (Base *)。若要访问派生类的 derivedMethod 方法,需要将指针临时转换为 Derived 类型。

星号指针类型必须用圆括号括起来。此外,强制转换表达式本身(包括变量名)也必须用另一对圆括号括起来。

在我们的测试中,还有另一个编译错误,提示“类型不匹配”-“类型不匹配”。这个错误出现在我们试图将一个 Rectangle 类型的指针强制转换成 Circle 类型的指针的那行代码上,因为这两个类处于不同的继承分支中。

   c = r// error: type mismatch

当强制类型转换的指针与实际对象不匹配时,情况会更加糟糕(尽管它们的类型兼容,因此程序可以正常编译)。这样的运算会在程序执行阶段就以错误结束(也就是说,编译器无法捕获错误)。程序随后会被卸载。

例如,在 ShapesCasting.mq5 脚本中,我们描述了一个指向 Square 的指针,并为其赋予了一个指向 Shape 的指针,而 Shape 包含 Rectangle 对象。

   Square *s2;
   // RUNTIME ERROR
   s2 = p// error: Incorrect casting of pointers

终端返回错误“指针强制转换不正确”。更具体的 Square 类型的指针无法指向父对象 Rectangle

为了避免运行时问题并防止程序崩溃,MQL5 提供了一个特殊的语言结构 dynamic_cast。使用此结构,您可以“仔细”检查是否可以将指针转换为所需的类型。如果可以转换,则编译器会进行转换。如果不可转换,我们将得到一个空指针 (NULL),我们可以用特殊方式处理它(例如,使用 if 以某种方式初始化或中断函数的执行,而不是整个程序)。

dynamic_cast 的语法如下:

dynamic_castClass * >( pointer )

在我们的例子中,只需编写:

   s2 = dynamic_cast<Square *>(p); // trying to cast type, and will get NULL if unsuccessful
   Print(s2); // 0

程序将按预期运行。

具体来说,我们可以再次尝试将矩形转换为圆形,并确保结果为 0:

   c = dynamic_cast<Circle *>(r); // trying to cast type, and will get NULL if unsuccessful
   Print(c); // 0

MQL5 中有一个特殊的指针类型,可以存储任何对象。此类型的表示法为:void *

我们来演示一下变量 void * 如何与 dynamic_cast 配合使用。

   void *v;
   v = s;   // set to the instance Square
   PRT(dynamic_cast<Shape *>(v));
   PRT(dynamic_cast<Rectangle *>(v));
   PRT(dynamic_cast<Square *>(v));
   PRT(dynamic_cast<Circle *>(v));
   PRT(dynamic_cast<Triangle *>(v));

前三行将记录指针(同一对象的描述符)的值,后两行将输出 0。

现在,回到 指标 一节中的前向声明示例(参见文件 ThisCallback.mq5),其中 ManagerElement 类包含相互的指针。

使用指针类型 void *,您可以删除初始声明 (ThisCallbackVoid.mq5)。我们注释掉包含它的那一行,并将包含对象管理器指针的 owner 字段类型更改为 void *。在构造函数中,我们也会更改参数的类型。

// class Manager; 
class Element
{
   void *owner// looking forward to being compatible with the Manager type *
public:
   Element(void *t = NULL): owner(t) { } // was Element(Manager &t)
   void doMath()
   {
      const int N = 1000000;
      
      // get the desired type at runtime
      Manager *ptr = dynamic_cast<Manager *>(owner);
      // then everywhere you need to check ptr for NULL before using
      
      for(int i = 0i < N; ++i)
      {
         if(i % (N / 20) == 0)
         {
            if(ptr != NULLptr.progressNotify(&thisi * 100.0f / N);
         }
         // ... lots of calculations
      }
      if(ptr != NULLptr.progressNotify(&this100.0f);
   }
   ...
};

这种方法可以提供更大的灵活性,但需要更加小心,因为 dynamic_cast 可能会返回 NULL。建议尽可能使用语言自身提供的类型控制的标准调度机制(静态和动态)。

通常,仅在特殊情况下才有必要使用 void * 指针。而带有初步说明的“额外”行不属于特殊情况。它在这里仅被用作 void * 指针通用性的最简单示例。