Преобразование из подкласса в суперкласс в подкласс? - PullRequest
6 голосов
/ 31 октября 2011

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

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

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

ОБНОВЛЕНИЕ: Спасибо, ребята, что ответили.Я не думаю, что это должно произойти, но это случилось.Я реализовал это по-другому, и теперь это работает.Однако, насколько я помню, я действительно объявил (чистую) виртуальную функцию в 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, стр. 183) для переопределения компилятора.В качестве альтернативы, мы можем запросить преобразование, которое проверяется во время выполнения, используя dynamic_cast, который описан в разделе 18.2.1 (стр. 773).

Ответы [ 3 ]

8 голосов
/ 31 октября 2011

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


Возможно, вам не нужно будет подбрасывать. Если все Note обеспечивают единообразное действие (скажем, 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 / и т. Д.). Если ваш компилятор поддерживает его, и RTTI не имеет существенных недостатков, он, безусловно, самый простой и требует меньше всего усилий с вашей стороны. Однако он не позволяет типу идентифицировать себя (хотя может быть доступна функция typeof).

    Это сделано, как у вас:

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

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

Комбинация последних двух методов некоторым способом (если пропущены Uuid 00-00-00-0000 и пункт назначения 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, требуют, чтобы объект предоставлял основную информацию о себе. Затем пользователь может попросить объект привести себя по мере необходимости, и объект имеет полное усмотрение относительно того, как он обрабатывается. Это обычно используется с сильно виртуальными классами или даже интерфейсами (чисто виртуальными), поскольку это может быть все знание, которое может иметь пользовательский код.

Этот метод, вероятно, бесполезен для вас в вашей области, но может представлять интерес и, безусловно, важен в отношении того, как типы могут идентифицировать себя и возвращаться "вверх" из базового класса или интерфейса.

2 голосов
/ 31 октября 2011

Используйте полиморфизм для доступа к различным реализациям для каждого из производных классов, как в следующем примере.

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;
}
0 голосов
/ 31 октября 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...