Conversion from subclass to superclass to subclass?

12,660

Solution 1

There are two significant trains of thought and code here, so shortest first:


You may not need to cast back up. If all Notes provide a uniform action (say Chime), then you can simply have:

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

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

and each Note will Chime as it should, using internal information (duration and pitch, for example).

This is clean, simple, and requires minimal code. It does mean the types all have to provide and inherit from a particular known interface/class, however.


Now the longer and far more involved methods occur when you do need to know the type and cast back up to it. There are two major methods, and a variant (#2) which may be used or combined with #3:

  1. This can be done in the compiler with RTTI (runtime type information), allowing it to safely dynamic_cast with good knowledge of what is allowed. This only works within a single compiler and perhaps single module (DLL/SO/etc), however. If your compiler supports it and there are no significant downsides of RTTI, it is by far the easiest and takes the least work on your end. It does not, however, allow the type to identify itself (although a typeof function may be available).

    This is done as you have:

    NewType * obj = dynamic_cast<NewType*>(obj_oldType);
    
  2. To make it entirely independent, adding a virtual method to the base class/interface (for example, Uuid GetType() const;) allows the object to identify itself at any time. This has a benefit over the third (true-to-COM) method, and a disadvantage: it allows the user of the object to make intelligent and perhaps faster decisions on what to do, but requires a) they cast (which may necessitate and unsafe reinterpret_cast or C-style cast) and b) the type cannot do any internal conversion or checking.

    ClassID id = obj->GetType();
    if (id == ID_Note_Long)
        NoteLong * note = (NoteLong*)obj;
        ...
    
  3. The option which COM uses is to provide a method of the form RESULT /* success */ CastTo(const Uuid & type, void ** ppDestination);. This allows the type to a) check the safety of the cast internally, b) perform the cast internally at its own discretion (there are rules on what can be done) and c) provide an error if the cast is impossible or fails. However, it a) prevents the user form optimizing well and b) may require multiple calls to find a succesful type.

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

Combining the latter two methods in some fashion (if a 00-00-00-0000 Uuid and nullptr destination are passed, fill the Uuid with the type's own Uuid, for example) may be the most optimal method of both identifying and safely converting types. Both the latter methods, and them combined, are compiler and API independent, and may even achieve language-independence with care (as COM does, in qualified manner).

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

The latter two are particularly useful when the type is almost entirely unknown: the source library, compiler, and even language are not known ahead of time, the only available information is that a given interface is provided. Working with such little data and unable to use highly compiler-specific features such as RTTI, requiring the object to provide basic information about itself is necessary. The user can then ask the object to cast itself as needed, and the object is has full discretion as to how that's handled. This is typically used with heavily virtual classes or even interfaces (pure virtual), as that may be all the knowledge the user code may have.

This method is probably not useful for you, in your scope, but may be of interest and is certainly important as to how types can identify themselves and be cast back "up" from a base class or interface.

Solution 2

Use polymorphism to access different implementations for the each of the derived classes like in the followin example.

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;
}
Share:
12,660
wecing
Author by

wecing

I'm just learning.

Updated on June 04, 2022

Comments

  • wecing
    wecing almost 2 years

    My program needs to handle different kinds of "notes": NoteShort, NoteLong... Different kinds of notes should be displayed in the GUI in different ways. I defined a base class of these notes, called NoteBase.

    I store these notes in XML; and I have a class which reads from the XML file and store notes' data in vector<NoteBase *> list. Then I found I cannot get their own types, because they are already converted to NoteBase *!

    Though if(dynamic_cast<NoteLong *>(ptr) != NULL) {...} may works, it's really too ugly. Implementing functions take NoteShort * or NoteLong * as parameter don't work. So, any good way to deal with this problem?

    UPDATE: Thank you guys for replying. I don't think it should happen neither -- but it did happened. I implemented it in another way, and it's now working. However, as far as I remember, I indeed declared the (pure) virtual function in NoteBase, but forgot to declare it again in headers of the deriving classes. I guess that's what caused the issue.

    UPDATE 2 (IMPORTANT): I found this quotation from C++ Primer, which may be helpful to others:

    What is sometimes a bit more surprising is that the restriction on converting from base to derived exists even when a base pointer or reference is actually bound to a derived object:

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

    The compiler has no way to know at compile time that a specific conversion will actually be safe at run time. The compiler looks only at the static types of the pointer or reference to determine whether a conversion is legal. In those cases when we know that the conversion from base to derived is safe, we can use a static_cast (Section 5.12.4, p. 183) to override the compiler. Alternatively, we could request a conversion that is checked at run time by using a dynamic_cast, which is covered in Section 18.2.1 (p. 773).