Преобразование из подкласс в суперкласс в подкласс?

моя программа должна обрабатывать различные виды "записок": NoteShort, NoteLong... Различные виды заметок должны отображаться в GUI по-разному. Я определил базовый класс этих заметок, называемый NoteBase.

я храню эти заметки в XML; и у меня есть класс, который читает из XML-файла и хранит данные заметок в vector<NoteBase *> list. Затем я обнаружил, что не могу получить их собственные типы, потому что они уже преобразованы в NoteBase *!

хотя if(dynamic_cast<NoteLong *>(ptr) != NULL) {...} может работает, это слишком уродливо. Осуществление функции NoteShort * или NoteLong * как параметр не работает. Итак, есть хороший способ справиться с этой проблемой?

UPDATE: Спасибо, ребята, за ответ. Я тоже не думаю, что это должно произойти, но это произошло. Я реализовал его по-другому, и теперь он работает. Однако, насколько я помню, я действительно объявил (чистую) виртуальную функцию в NoteBase, но забыли объявить его снова в заголовках производных классов. Я думаю, это то, что вызвало вопрос.

ОБНОВЛЕНИЕ 2 (ВАЖНО): Я нашел эту цитату из C++ Primer, которая может быть полезна другим:

что иногда немного более удивительно, так это то, что ограничение на преобразование из базового в производный существует, даже если базовый указатель или ссылка фактически привязана к производному объекту:

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

компилятор не может знать во время компиляции, что определенный преобразование на самом деле будет безопасным во время выполнения. Компилятор выглядит только в статических видах указатель или ссылку на определения преобразование является законным. В тех случаях, когда мы знаем, что преобразование от основания к производному безопасно, мы можем использовать static_cast (раздел 5.12.4, p. 183) для переопределения компилятора. Кроме того, мы можем запросить преобразование, которое проверяется во время выполнения с помощью dynamic_cast, который рассматривается в разделе 18.2.1 (стр. 773).

3 ответов


здесь есть два значительных потока мысли и кода, поэтому самый короткий первый:


возможно, Вам не нужно бросать обратно. Если все Notes обеспечивают равномерное действие (скажем Chime), то вы можете просто:

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

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

и друг Note будет Chime как и должно, используя внутреннюю информацию (например, продолжительность и шаг).

это чистый, простой, и требует минимального кода. Это означает, что все типы должны предоставлять и наследовать однако из определенного известного интерфейса / класса.


теперь более длинные и гораздо более сложные методы происходят, когда вы do нужно знать тип и бросить его. Существует два основных метода и вариант (#2), который можно использовать или комбинировать с #3:

  1. это можно сделать в компиляторе с RTTI (информация о типе среды выполнения), что позволяет безопасно dynamic_cast С хорошим знанием того, что разрешено. Это только работает в однако один компилятор и, возможно, один модуль (DLL/SO / etc). Если ваш компилятор поддерживает его, и нет существенных недостатков RTTI, это намного проще и занимает наименьшую работу на вашем конце. Однако он не позволяет типу идентифицировать себя (хотя a typeof функция может быть доступна).

    это делается так, как вы сделали:

    NewType * obj = dynamic_cast<NewType*>(obj_oldType);
    
  2. чтобы сделать его полностью независимым, добавив виртуальный метод в базовый класс / интерфейс (для пример,Uuid GetType() const;) позволяет объекту идентифицировать себя в любой момент. Это имеет преимущество перед третьим (true-to-COM) методом и недостаток: он позволяет пользователю объекта принимать интеллектуальные и, возможно, более быстрые решения о том, что делать, но требует a) они бросают (что может потребовать и небезопасно reinterpret_cast или C-style cast) и 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;
}

как указывали другие, вы должны попытаться спроектировать базовый класс таким образом, чтобы вы могли делать все необходимое без кастинга. Если это невозможно (то есть, если вам нужна информация, специфичная для подклассов), вы можете использовать кастинг, как вы это сделали, или вы можете использовать double-dispatch.