繁体   English   中英

从子类转换到超类到子类?

[英]Conversion from subclass to superclass to subclass?

我的程序需要处理不同类型的“注释”: NoteShortNoteLong ...应该以不同的方式在GUI中显示不同类型的注释。 我定义了这些注释的基类,称为NoteBase

我将这些笔记存储在XML中; 我有一个类从XML文件读取并将注释的数据存储在vector<NoteBase *> list 然后我发现我无法获得自己的类型,因为它们已经转换为NoteBase *

虽然if(dynamic_cast<NoteLong *>(ptr) != NULL) {...}可能有效,但实在太难看了。 实现函数使用NoteShort *NoteLong *作为参数不起作用。 那么,有什么好方法可以解决这个问题吗?

更新:谢谢各位回复。 我认为它不应该发生 - 但它确实发生了。 我以另一种方式实现了它,它现在正在工作。 但是,据我记得,我确实在NoteBase声明了(纯)虚函数,但忘了在派生类的头文件中再次声明它。 我猜这就是造成这个问题的原因。

更新2(重要):我从C ++ Primer中找到了这个引用,这可能对其他人有帮助:

有时更令人惊讶的是,即使基本指针或引用实际绑定到派生对象,也存在从base转换为派生的限制:

  Bulk_item bulk; Item_base *itemP = &bulk; // ok: dynamic type is Bulk_item Bulk_item *bulkP = itemP; // error: can't convert base to derived 

编译器无法在编译时知道特定转换在运行时实际上是安全的。 编译器仅查看指针或引用的静态类型,以确定转换是否合法。 在我们知道从base到derived的转换是安全的情况下,我们可以使用static_cast(第5.12.4节,第183页)来覆盖编译器。 或者,我们可以使用dynamic_cast请求在运行时检查的转换,这将在第18.2.1节(第773页)中介绍。

这里有两个重要的思路和代码,首先是最短的:


您可能不需要重新投票。 如果所有Note提供统一的动作(比如Chime ),那么你可以简单地:

class INote
{
    virtual void Chime() = 0;
};

...
for_each(INote * note in m_Notes)
{
    note->Chime();
}

和每个NoteChime ,因为它应该使用的内部信息(持续时间和音调,例如)。

这很干净,简单,只需要很少的代码。 但它确实意味着所有类型都必须提供并继承自特定的已知接口/类。


现在,当您确实需要知道类型并将其转换回来时,会出现更长且涉及更多的方法。 有两种主要方法,一种可以与#3一起使用或组合的变体(#2):

  1. 这可以在带有RTTI(运行时类型信息)的编译器中完成,允许它安全地dynamic_cast并且知道允许的内容。 但是,这仅适用于单个编译器和单个模块(DLL / SO / etc)。 如果您的编译器支持它并且RTTI没有明显的缺点,那么它是迄今为止最简单的并且最少的工作。 但是,它不允许类型识别自身(尽管可以使用typeof函数)。

    这是按照您的方式完成的:

     NewType * obj = dynamic_cast<NewType*>(obj_oldType); 
  2. 为了使其完全独立,向基类/接口添加虚方法(例如, Uuid GetType() const; )允许对象随时标识自己。 这比第三种(真正的COM)方法有一个好处,也是一个缺点:它允许对象的用户做出明智的,也许更快的决定做什么,但需要a)他们施放(这可能需要和不安全的reinterpret_cast或C风格的演员表)和b)该类型不能进行任何内部转换或检查。

     ClassID id = obj->GetType(); if (id == ID_Note_Long) NoteLong * note = (NoteLong*)obj; ... 
  3. COM使用的选项是提供一种形式为RESULT /* success */ CastTo(const Uuid & type, void ** ppDestination); 这允许类型a)检查内部铸件的安全性,b)由内部自行决定执行铸造(有关于可以做什么的规则)和c)如果铸造不可能或失败则提供错误。 但是,它a)阻止用户表单优化,b)可能需要多次调用才能找到成功的类型。

     NoteLong * note = nullptr; if (obj->GetAs(ID_Note_Long, &note)) ... 

以某种方式组合后两种方法(如果传递00-00-00-0000 Uuid和nullptr目的地,例如用类型自己的Uuid填充Uuid)可能是识别和安全转换类型的最佳方法。 后两种方法和它们相结合,都是编译器和API独立的,甚至可以小心地实现语言独立性(正如COM那样,以合格的方式)。

ClassID id = ClassID::Null;
obj->GetAs(id, nullptr);
if (id == ID_Note_Long)
    NoteLong * note;
    obj->GetAs(ID_Note_Long, &note);
    ...

当类型几乎完全未知时,后两者特别有用:源库,编译器甚至语言都不是提前知道的,唯一可用的信息是提供给定的接口。 使用这些小数据并且无法使用高度特定于编译器的功能(如RTTI),需要对象提供有关其自身的基本信息。 然后,用户可以根据需要请求对象自我投射,并且对象可以完全自行决定如何处理。 这通常与大量虚拟类甚至接口(纯虚拟)一起使用,因为这可能是用户代码可能具有的所有知识。

在您的范围内,此方法可能对您没有用,但可能是有意义的,并且对于类型如何标识自身并从基类或接口“向上”转换而言当然很重要。

使用多态来访问每个派生类的不同实现,如下面的示例所示。

class NoteBase
{
  public:
    virtual std::string read() = 0;
};

class NoteLong : public NoteBase
{
  public:
    std::string read() override { return "note long"; }
};

class NoteShort : public NoteBase
{
  public:
    std::string read() override { return "note short"; }
};

int main()
{
  std::vector< NoteBase* > notes;
  for( int i=0; i<10; ++i )
  {
    if( i%2 )
      notes.push_back(new NoteLong() );
    else
      notes.push_back( new NoteShort() );
  }

  std::vector< NoteBase* >::iterator it;
  std::vector< NoteBase* >::iterator end = notes.end();
  for( it=notes.begin(); it != end; ++it )
    std::cout << (*it)->read() << std::endl;

  return 0;
}

正如其他人所指出的那样,你应该尝试设计基类,让你可以在不进行投射的情况下完成所需的所有工作。 如果这不可能(也就是说,如果您需要特定于子类的信息),您可以像使用过一样使用强制转换,也可以使用双重分派。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM