У меня довольно простой вопрос об операторе dynamic_cast
. Я знаю, что это используется для идентификации типа во время выполнения, т. Е. Чтобы узнать о типе объекта во время выполнения. Но, исходя из вашего опыта программирования, не могли бы вы привести реальный сценарий, в котором вам приходилось использовать этот оператор? Какие были трудности без его использования?
Практическое использование dynamic_cast?
Ответы (9)
Пример игрушки
Ноев ковчег будет служить вместилищем для разных видов животных. Поскольку сам ковчег не заботится о разнице между обезьянами, пингвинами и комарами, вы определяете класс Animal
, выводите из него классы Monkey
, Penguin
и Mosquito
и сохраняете каждый из них как Animal
в ковчеге.
Когда потоп закончится, Ной хочет распределить животных по земле по местам, которым они принадлежат, и, следовательно, ему нужны дополнительные знания об общих животных, хранящихся в его ковчеге. Например, теперь он может попытаться dynamic_cast<>
определить каждое животное по Penguin
, чтобы выяснить, какие из животных являются пингвинами, которых следует выпустить в Антарктику, а какие нет.
Пример из жизни
Мы внедрили инфраструктуру мониторинга событий, в которой приложение будет хранить события, сгенерированные во время выполнения, в виде списка. Мониторы событий просматривают этот список и изучают те конкретные события, которые их интересуют. Типы событий — это вещи уровня ОС, такие как SYSCALL
, FUNCTIONCALL
и INTERRUPT
.
Здесь мы сохранили все наши конкретные события в общем списке Event
экземпляров. Затем мониторы будут перебирать этот список и dynamic_cast<>
события, которые они видели, к тем типам, которые их интересуют. Все остальные (те, которые вызывают исключение) игнорируются.
Вопрос. Почему нельзя создать отдельный список для каждого типа событий?
Ответ: вы можете сделать это, но это усложняет расширение системы новыми событиями, а также новыми мониторами (объединяющими несколько типов событий), поскольку каждый должен знать соответствующие списки для проверки.
visitor->visit(this)
и повторно реализуется для каждого производного класса иерархии. Обратите внимание, что this
каждый раз имеет другой тип и поэтому вызывает другую перегрузку visitor::visit
.
- person Alexandre C.; 03.08.2012
Типичным примером использования является шаблон посетителя:
struct Element
{
virtual ~Element() { }
void accept(Visitor & v)
{
v.visit(this);
}
};
struct Visitor
{
virtual void visit(Element * e) = 0;
virtual ~Visitor() { }
};
struct RedElement : Element { };
struct BlueElement : Element { };
struct FifthElement : Element { };
struct MyVisitor : Visitor
{
virtual void visit(Element * e)
{
if (RedElement * p = dynamic_cast<RedElement*>(e))
{
// do things specific to Red
}
else if (BlueElement * p = dynamic_cast<BlueElement*>(e))
{
// do things specific to Blue
}
else
{
// error: visitor doesn't know what to do with this element
}
}
};
Теперь, если у вас есть Element & e;
, вы можете сделать MyVisitor v;
и сказать e.accept(v)
.
Ключевой особенностью дизайна является то, что если вы измените свою Element
иерархию, вам нужно будет отредактировать только своих посетителей. Шаблон по-прежнему довольно сложен и рекомендуется только в том случае, если у вас очень стабильная иерархия классов Element
s.
visit
для принятия различных производных Element
, вы можете вообще избежать необходимости в dynamic_cast
. Я думаю, что это главное преимущество шаблона Посетитель (Element
объекты раскрывают своим посетителям свой конкретный тип).
- person Andrew Durward; 01.08.2012
Element
, не? В любом случае, есть и другие способы реализации посетителя, но это хороший пример того, где полезны динамические приведения типов.
- person Kerrek SB; 01.08.2012
accept
обычно реализуется в каждом производном классе Element
(или с CRTP). Таким образом, тип this
известен во время компиляции.
- person Andrew Durward; 01.08.2012
dynamic_cast
switch в каждом посетителе, если это вообще возможно (DRY). Моя главная мысль здесь заключается в том, что я не согласен с утверждением, что типичным вариантом использования [динамического приведения] является шаблон посетителя.
- person Andrew Durward; 01.08.2012
Представьте себе такую ситуацию: у вас есть программа на C++, которая читает и отображает HTML. У вас есть базовый класс HTMLElement
, который имеет чисто виртуальный метод displayOnScreen
. У вас также есть функция renderHTMLToBitmap
, которая рисует HTML в растровое изображение. Если у каждого HTMLElement
есть vector<HTMLElement*> children;
, вы можете просто передать HTMLElement
, представляющий элемент <html>
. Но что, если несколько подклассов нуждаются в особой обработке, например <link>
для добавления CSS. Вам нужен способ узнать, является ли элемент LinkElement
, чтобы вы могли передать его функциям CSS. Чтобы узнать это, вы должны использовать dynamic_cast
.
Проблема с dynamic_cast
и полиморфизмом в целом заключается в том, что он не очень эффективен. Когда вы добавляете в микс vtables, становится только хуже.
Когда вы добавляете в базовый класс виртуальные функции, при их вызове вы в конечном итоге проходите довольно много слоев указателей на функции и областей памяти. Это никогда не будет более эффективным, чем что-то вроде инструкции ASM call
.
Редактировать: в ответ на комментарий Эндрю ниже, вот новый подход: вместо динамического приведения к определенному типу элемента (LinkElement
) вместо этого у вас есть другой абстрактный подкласс HTMLElement
, называемый ActionElement
, который переопределяет displayOnScreen
с функцией, которая ничего не отображает, и создает новая чисто виртуальная функция: virtual void doAction() const = 0
. dynamic_cast
изменяется на проверку ActionElement
и просто вызывает doAction()
. У вас будет такой же подкласс для GraphicalElement
с виртуальным методом displayOnScreen()
.
Редактировать 2: Вот как может выглядеть метод «рендеринга»:
void render(HTMLElement root) {
for(vector<HTLMElement*>::iterator i = root.children.begin(); i != root.children.end(); i++) {
if(dynamic_cast<ActionElement*>(*i) != NULL) //Is an ActionElement
{
ActionElement* ae = dynamic_cast<ActionElement*>(*i);
ae->doAction();
render(ae);
}
else if(dynamic_cast<GraphicalElement*>(*i) != NULL) //Is a GraphicalElement
{
GraphicalElement* ge = dynamic_cast<GraphicalElement*>(*i);
ge->displayToScreen();
render(ge);
}
else
{
//Error
}
}
}
dynamic_cast
. Метод, который должен проверять конкретный тип объекта, не соответствует Open- Принцип закрытости. Что произойдет, если вы добавите новый подтип в свою иерархию? Затем вы должны найти все места в вашем коде, где вы хотите применить особое поведение.
- person Andrew Durward; 01.08.2012
dynamic_cast
? Его цель — проверить указатель базового класса на предмет того, указывает ли он на дочерний элемент указанного типа. Это подразумевает необходимость добавления дополнительных проверок для новых типов. И да, если бы мне пришлось писать анализатор HTML, я бы использовал структуру данных для сопоставления тегов с функциями рендеринга, но это не мешает этому быть верным.
- person Linuxios; 01.08.2012
doAction
, то почему можно хранить объекты без такого метода?
- person Andrew Durward; 01.08.2012
element->render()
? Пусть производные классы выяснят, что они хотят делать.
- person Andrew Durward; 01.08.2012
render()
. Элементы <script>
не имеют способа рендеринга, так почему они должны быть вынуждены реализовывать этот метод. Это просто не имеет смысла. В любом случае, это тот случай, когда можно использовать dynamic_cast
, я согласен с вами, что некоторые виртуальные функции проще в обычном программировании.
- person Linuxios; 01.08.2012
Оператор dynamic_cast
решает ту же проблему, что и динамическая диспетчеризация (виртуальные функции, шаблон посетителя и т. д.): он позволяет выполнять различные действия в зависимости от типа объекта во время выполнения.
Тем не менее, вы всегда должны отдавать предпочтение динамической диспетчеризации, за исключением, возможно, случаев, когда количество dynamic_cast
, которое вам нужно, никогда не будет расти.
Например. вы никогда не должны делать:
if (auto v = dynamic_cast<Dog*>(animal)) { ... }
else if (auto v = dynamic_cast<Cat*>(animal)) { ... }
...
по соображениям ремонтопригодности и производительности, но вы можете сделать, например.
for (MenuItem* item: items)
{
if (auto submenu = dynamic_cast<Submenu*>(item))
{
auto items = submenu->items();
draw(context, items, position); // Recursion
...
}
else
{
item->draw_icon();
item->setup_accelerator();
...
}
}
что я нашел весьма полезным в этой конкретной ситуации: у вас есть одна очень конкретная подиерархия, которую нужно обрабатывать отдельно, вот где сияет dynamic_cast
. Но примеры из реального мира довольно редки (пример меню — это то, с чем мне приходилось иметь дело).
dynamic_cast не предназначен в качестве альтернативы виртуальным функциям.
dynamic_cast имеет нетривиальные накладные расходы на производительность (по крайней мере, я так думаю), поскольку необходимо пройти всю иерархию классов.
dynamic_cast похож на оператор is в C# и QueryInterface в старом добром COM.
На данный момент я нашел одно реальное применение dynamic_cast:
(*) у вас есть множественное наследование, и чтобы найти цель приведения, компилятор должен пройти по классу вверх и вниз по иерархии, чтобы найти цель (или вниз и вверх, если хотите). Это означает, что цель приведения находится в параллельной ветви относительно того, где в иерархии находится источник приведения. Я думаю, что НЕТ другого способа сделать такой бросок.
Во всех остальных случаях вы просто используете некоторый виртуальный базовый класс, чтобы сообщить вам, какой тип объекта у вас есть, и ТОЛЬКО ПОСЛЕ ТОГО вы динамически_приводите его к целевому классу, чтобы вы могли использовать некоторые его не виртуальные функции. В идеале не должно быть невиртуального функционала, но черт возьми, мы же живем в реальном мире.
Выполнение таких действий, как:
if (v = dynamic_cast(...)){} else if (v = dynamic_cast(...)){} else if ...
это потеря производительности.
По возможности следует избегать приведения типов, потому что это, по сути, говорит компилятору, что вы лучше знаете, и обычно это признак более слабого проектного решения.
Однако вы можете столкнуться с ситуациями, когда уровень абстракции был слишком высок для 1 или 2 подклассов, когда у вас есть выбор: изменить свой дизайн или решить проблему, проверив подкласс с помощью dynamic_cast и обработав его в отдельной ветке. Компромисс заключается в добавлении дополнительного времени и риска сейчас против дополнительных проблем с обслуживанием позже.
dynamic_cast
отличается от кастинга. Это означает, что вы запрашиваете у компилятора информацию, которой вы не знаете, что позволит вам делать разные вещи в зависимости от конкретных подклассов в полиморфизме.
- person Linuxios; 01.08.2012
int
s в char
s и long
s в struct {short i1; short i2;}
s. Это более контролируемо.
- person Linuxios; 01.08.2012
В большинстве ситуаций, когда вы пишете код, в котором вы знаете тип объекта, с которым работаете, вы просто используете static_cast, так как он более эффективен.
Ситуации, когда вам нужно динамическое приведение, обычно возникают (по моему опыту) из-за отсутствия предвидения в дизайне - обычно, когда дизайнер не может предоставить перечисление или идентификатор, который позволяет вам определить тип позже в коде.
Например, я видел эту ситуацию уже более чем в одном проекте:
Вы можете использовать фабрику, где внутренняя логика решает, какой производный класс хочет пользователь, а не пользователь явно выбирает его. Эта фабрика в идеальном мире возвращает перечисление, которое поможет вам определить тип возвращаемого объекта, но если это не так, вам может потребоваться проверить, какой тип объекта он вам дал, с помощью dynamic_cast.
Ваш дополнительный вопрос, очевидно, будет следующим: зачем вам знать тип объекта, который вы используете в коде с помощью фабрики?
В идеальном мире вы бы этого не сделали — интерфейса, предоставляемого базовым классом, было бы достаточно для управления всеми объектами, возвращаемыми фабриками, во всех необходимых пределах. Однако люди не проектируют идеально. Например, если ваша фабрика создает абстрактные объекты соединения, вы можете внезапно понять, что вам нужно получить доступ к флагу UseSSL в вашем объекте соединения сокета, но фабричная база не поддерживает это, и это не относится ни к одному из других классов, использующих интерфейс. Итак, возможно, вы могли бы проверить, используете ли вы этот тип производного класса в своей логике, и напрямую установить/установить флаг, если да.
Это уродливо, но это не идеальный мир, и иногда у вас нет времени на полный рефакторинг несовершенного дизайна в реальном мире из-за загруженности работой.
type
или id
базового класса, которое будет проверено с помощью switch
и обработано с помощью dynamic_cast
. Для меня это просто похоже на повторную реализацию того, что компилятор уже сделал: создайте поле типа и запросите его, чтобы узнать, можете ли вы привести его к типу. И компилятор, вероятно, делает это намного эффективнее, чем вы могли бы в коде.
- person Linuxios; 01.08.2012
Оператор dynamic_cast очень полезен для меня. Особенно я использую его с шаблоном Observer для управления событиями:
#include <vector>
#include <iostream>
using namespace std;
class Subject; class Observer; class Event;
class Event { public: virtual ~Event() {}; };
class Observer { public: virtual void onEvent(Subject& s, const Event& e) = 0; };
class Subject {
private:
vector<Observer*> m_obs;
public:
void attach(Observer& obs) { m_obs.push_back(& obs); }
public:
void notifyEvent(const Event& evt) {
for (vector<Observer*>::iterator it = m_obs.begin(); it != m_obs.end(); it++) {
if (Observer* const obs = *it) {
obs->onEvent(*this, evt);
}
}
}
};
// Define a model with events that contain data.
class MyModel : public Subject {
public:
class Evt1 : public Event { public: int a; string s; };
class Evt2 : public Event { public: float f; };
};
// Define a first service that processes both events with their data.
class MyService1 : public Observer {
public:
virtual void onEvent(Subject& s, const Event& e) {
if (const MyModel::Evt1* const e1 = dynamic_cast<const MyModel::Evt1*>(& e)) {
cout << "Service1 - event Evt1 received: a = " << e1->a << ", s = " << e1->s << endl;
}
if (const MyModel::Evt2* const e2 = dynamic_cast<const MyModel::Evt2*>(& e)) {
cout << "Service1 - event Evt2 received: f = " << e2->f << endl;
}
}
};
// Define a second service that only deals with the second event.
class MyService2 : public Observer {
public:
virtual void onEvent(Subject& s, const Event& e) {
// Nothing to do with Evt1 in Service2
if (const MyModel::Evt2* const e2 = dynamic_cast<const MyModel::Evt2*>(& e)) {
cout << "Service2 - event Evt2 received: f = " << e2->f << endl;
}
}
};
int main(void) {
MyModel m; MyService1 s1; MyService2 s2;
m.attach(s1); m.attach(s2);
MyModel::Evt1 e1; e1.a = 2; e1.s = "two"; m.notifyEvent(e1);
MyModel::Evt2 e2; e2.f = .2f; m.notifyEvent(e2);
}
Контрактное программирование и RTTI показывает, как вы можете использовать dynamic_cast
, чтобы разрешить объектам рекламировать какие интерфейсы они реализуют. Мы использовали его в моем магазине, чтобы заменить довольно непрозрачную систему метаобъектов. Теперь мы можем четко описать функциональность объектов, даже если объекты вводятся новым модулем через несколько недель/месяцев после того, как платформа была «запечена» (хотя, конечно, контракты должны быть определены заранее).