Практическое использование dynamic_cast?

У меня довольно простой вопрос об операторе dynamic_cast. Я знаю, что это используется для идентификации типа во время выполнения, т. Е. Чтобы узнать о типе объекта во время выполнения. Но, исходя из вашего опыта программирования, не могли бы вы привести реальный сценарий, в котором вам приходилось использовать этот оператор? Какие были трудности без его использования?


person cexplorer    schedule 01.08.2012    source источник


Ответы (9)


Пример игрушки

Ноев ковчег будет служить вместилищем для разных видов животных. Поскольку сам ковчег не заботится о разнице между обезьянами, пингвинами и комарами, вы определяете класс Animal, выводите из него классы Monkey, Penguin и Mosquito и сохраняете каждый из них как Animal в ковчеге.

Когда потоп закончится, Ной хочет распределить животных по земле по местам, которым они принадлежат, и, следовательно, ему нужны дополнительные знания об общих животных, хранящихся в его ковчеге. Например, теперь он может попытаться dynamic_cast<> определить каждое животное по Penguin, чтобы выяснить, какие из животных являются пингвинами, которых следует выпустить в Антарктику, а какие нет.

Пример из жизни

Мы внедрили инфраструктуру мониторинга событий, в которой приложение будет хранить события, сгенерированные во время выполнения, в виде списка. Мониторы событий просматривают этот список и изучают те конкретные события, которые их интересуют. Типы событий — это вещи уровня ОС, такие как SYSCALL, FUNCTIONCALL и INTERRUPT.

Здесь мы сохранили все наши конкретные события в общем списке Event экземпляров. Затем мониторы будут перебирать этот список и dynamic_cast<> события, которые они видели, к тем типам, которые их интересуют. Все остальные (те, которые вызывают исключение) игнорируются.

Вопрос. Почему нельзя создать отдельный список для каждого типа событий?

Ответ: вы можете сделать это, но это усложняет расширение системы новыми событиями, а также новыми мониторами (объединяющими несколько типов событий), поскольку каждый должен знать соответствующие списки для проверки.

person BjoernD    schedule 01.08.2012
comment
Этот мониторинг событий был действительно очень практичным примером, я мог ясно представить, насколько важен динамический приведение. большое спасибо!!! - person cexplorer; 01.08.2012
comment
Создал ли он arch после ark? - person Daniel; 01.08.2012
comment
Упс. Иногда быть неродным больно. ;) - person BjoernD; 01.08.2012
comment
Ничего страшного. Я подумал, что, возможно, он продолжил карьеру в архитектуре. :) - person Daniel; 01.08.2012
comment
Я категорически не согласен с примером 2: как только вы добавляете более сложные мониторы, вы рискуете прописать внутри логики монитора строки динамических приведений. Это лучше всего обрабатывается с помощью шаблона посетителя (это лучше всего обрабатывается с помощью сопоставления с образцом, а в языках ООП сопоставление с образцом достигается с помощью шаблона посетителя). Есть способы безболезненно добавлять новые типы событий (просто напишите реализации по умолчанию в базовом классе посетителей, которые делегируют базовому классу). - person Alexandre C.; 02.08.2012
comment
Но в какой-то момент во время вашего вызова функции visit() вам все равно нужно будет выяснить, на какой объект вы смотрите? - person BjoernD; 02.08.2012
comment
@BjoernD: это задача функции accept () по отправке на правильный визит () посетителя. В методе visit() у вас есть объект нужного типа. - person Alexandre C.; 02.08.2012
comment
@BjoernD: функция принятия вызывает visitor->visit(this) и повторно реализуется для каждого производного класса иерархии. Обратите внимание, что this каждый раз имеет другой тип и поэтому вызывает другую перегрузку visitor::visit. - person Alexandre C.; 03.08.2012
comment
dynamic_cast не предназначен для замены виртуальных функций. Вы можете легко определить виртуальную функцию базового класса GetAnimalType и заставить ее возвращать GUID, если вы не хотите писать перечисление и динамически расширяться с помощью неизвестных обработчиков событий. - person Dimitrios Staikos; 08.08.2012
comment
Димитрис Стайкос: Конечно. Но тогда у вас возникает проблема с управлением идентификаторами GUID в вашей программе. Или даже между модулями, поступающими из разных источников, где у вас есть только бинарные версии. В этих случаях dynamic_cast может быть лучшим способом. - person BjoernD; 08.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 иерархию, вам нужно будет отредактировать только своих посетителей. Шаблон по-прежнему довольно сложен и рекомендуется только в том случае, если у вас очень стабильная иерархия классов Elements.

person Kerrek SB    schedule 01.08.2012
comment
К сожалению, перегружая метод visit для принятия различных производных Element, вы можете вообще избежать необходимости в dynamic_cast. Я думаю, что это главное преимущество шаблона Посетитель (Element объекты раскрывают своим посетителям свой конкретный тип). - person Andrew Durward; 01.08.2012
comment
@AndrewDurward: разрешение перегрузки происходит статически. Динамический бросок происходит динамически. Не думаю, что они решают одну и ту же проблему. В вашем подходе потребуется добавить код в каждый класс, производный от Element, не? В любом случае, есть и другие способы реализации посетителя, но это хороший пример того, где полезны динамические приведения типов. - person Kerrek SB; 01.08.2012
comment
Правильно, поэтому accept обычно реализуется в каждом производном классе Element (или с CRTP). Таким образом, тип this известен во время компиляции. - person Andrew Durward; 01.08.2012
comment
+1, потому что это менее связанный дизайн по сравнению с тем, который использует промежуточный уровень диспетчеризации внутри элементов (как в примере Java в статье Википедии en.wikipedia.org/wiki/Visitor_pattern). Плюсы: 1) Элемент вообще не должен знать о посетителях. 2) посетителю не нужно знать об элементах, которые не интересны данному посетителю. - person user396672; 01.08.2012
comment
@AndrewDurward Вы действительно предпочли бы сложность CRTP этому, если бы у вас не было встроенной системы с очень небольшими ресурсами? Я думаю, что простота выигрывает в этом компромиссе. - person John Humphreys; 01.08.2012
comment
@w00te Может быть, а может и нет. Но я бы определенно избегал повторения оператора 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
    }
  }
}
person Linuxios    schedule 01.08.2012
comment
Полиморфизм C++ и vtables обычно более эффективны, чем другие способы решения подобных проблем. В наши дни это редко вызывает беспокойство, если только вы не ориентируетесь на встроенную платформу или что-то в этом роде. - person aschepler; 01.08.2012
comment
Большое спасибо линуксоидам!! это был действительно очень хороший пример! Теперь я понимаю, когда нам нужно использовать какой-то общий элемент/переменную класса, имеющий различные указатели на его производные, и могут быть случаи, когда нам нужно специально обрабатывать некоторые производные, тогда мы используем динамическое приведение. Большое спасибо! Престижность как Linuxios, так и BjoernD! - person cexplorer; 01.08.2012
comment
На мой взгляд, пример, который вы представили, не является хорошим использованием dynamic_cast. Метод, который должен проверять конкретный тип объекта, не соответствует Open- Принцип закрытости. Что произойдет, если вы добавите новый подтип в свою иерархию? Затем вы должны найти все места в вашем коде, где вы хотите применить особое поведение. - person Andrew Durward; 01.08.2012
comment
@aschepler: я знаю, но по сравнению с основными вызовами функций и методов это гораздо менее эффективно. - person Linuxios; 01.08.2012
comment
@AndrewDurward: Верно и согласен. Но какая еще польза от dynamic_cast? Его цель — проверить указатель базового класса на предмет того, указывает ли он на дочерний элемент указанного типа. Это подразумевает необходимость добавления дополнительных проверок для новых типов. И да, если бы мне пришлось писать анализатор HTML, я бы использовал структуру данных для сопоставления тегов с функциями рендеринга, но это не мешает этому быть верным. - person Linuxios; 01.08.2012
comment
Я бы сказал, что это улучшение, но, на мой взгляд, все еще есть проблема дизайна. Если ваш контейнер требует, чтобы у объектов был интерфейс с методом doAction, то почему можно хранить объекты без такого метода? - person Andrew Durward; 01.08.2012
comment
Что не так с element->render()? Пусть производные классы выяснят, что они хотят делать. - person Andrew Durward; 01.08.2012
comment
@AndrewDurward: Потому что это предотвращает класс, который по хорошему дизайну не должен иметь даже метода 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. Но примеры из реального мира довольно редки (пример меню — это то, с чем мне приходилось иметь дело).

person Alexandre C.    schedule 02.08.2012

dynamic_cast не предназначен в качестве альтернативы виртуальным функциям.
dynamic_cast имеет нетривиальные накладные расходы на производительность (по крайней мере, я так думаю), поскольку необходимо пройти всю иерархию классов.
dynamic_cast похож на оператор is в C# и QueryInterface в старом добром COM.

На данный момент я нашел одно реальное применение dynamic_cast:
(*) у вас есть множественное наследование, и чтобы найти цель приведения, компилятор должен пройти по классу вверх и вниз по иерархии, чтобы найти цель (или вниз и вверх, если хотите). Это означает, что цель приведения находится в параллельной ветви относительно того, где в иерархии находится источник приведения. Я думаю, что НЕТ другого способа сделать такой бросок.

Во всех остальных случаях вы просто используете некоторый виртуальный базовый класс, чтобы сообщить вам, какой тип объекта у вас есть, и ТОЛЬКО ПОСЛЕ ТОГО вы динамически_приводите его к целевому классу, чтобы вы могли использовать некоторые его не виртуальные функции. В идеале не должно быть невиртуального функционала, но черт возьми, мы же живем в реальном мире.

Выполнение таких действий, как:

    if (v = dynamic_cast(...)){} else if (v = dynamic_cast(...)){} else if ...

это потеря производительности.

person Dimitrios Staikos    schedule 08.08.2012

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

Однако вы можете столкнуться с ситуациями, когда уровень абстракции был слишком высок для 1 или 2 подклассов, когда у вас есть выбор: изменить свой дизайн или решить проблему, проверив подкласс с помощью dynamic_cast и обработав его в отдельной ветке. Компромисс заключается в добавлении дополнительного времени и риска сейчас против дополнительных проблем с обслуживанием позже.

person stefaanv    schedule 01.08.2012
comment
спасибо, stefaanv, это был совсем другой аспект, который вы объяснили. - person cexplorer; 01.08.2012
comment
dynamic_cast отличается от кастинга. Это означает, что вы запрашиваете у компилятора информацию, которой вы не знаете, что позволит вам делать разные вещи в зависимости от конкретных подклассов в полиморфизме. - person Linuxios; 01.08.2012
comment
@Linuxios: я не согласен, это все еще кастинг - person stefaanv; 01.08.2012
comment
@stefaanv: это все еще кастинг, но он не превращает ints в chars и longs в struct {short i1; short i2;}s. Это более контролируемо. - person Linuxios; 01.08.2012

В большинстве ситуаций, когда вы пишете код, в котором вы знаете тип объекта, с которым работаете, вы просто используете static_cast, так как он более эффективен.

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

Например, я видел эту ситуацию уже более чем в одном проекте:

Вы можете использовать фабрику, где внутренняя логика решает, какой производный класс хочет пользователь, а не пользователь явно выбирает его. Эта фабрика в идеальном мире возвращает перечисление, которое поможет вам определить тип возвращаемого объекта, но если это не так, вам может потребоваться проверить, какой тип объекта он вам дал, с помощью dynamic_cast.

Ваш дополнительный вопрос, очевидно, будет следующим: зачем вам знать тип объекта, который вы используете в коде с помощью фабрики?

В идеальном мире вы бы этого не сделали — интерфейса, предоставляемого базовым классом, было бы достаточно для управления всеми объектами, возвращаемыми фабриками, во всех необходимых пределах. Однако люди не проектируют идеально. Например, если ваша фабрика создает абстрактные объекты соединения, вы можете внезапно понять, что вам нужно получить доступ к флагу UseSSL в вашем объекте соединения сокета, но фабричная база не поддерживает это, и это не относится ни к одному из других классов, использующих интерфейс. Итак, возможно, вы могли бы проверить, используете ли вы этот тип производного класса в своей логике, и напрямую установить/установить флаг, если да.

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

person John Humphreys    schedule 01.08.2012
comment
Привет, W00te, то, что вы говорите, интересно. Не могли бы вы объяснить, как мы могли бы использовать статический оператор приведения в примере мониторинга событий, приведенном BjoernD выше. - person cexplorer; 01.08.2012
comment
@cexplorer: для этого потребуется использовать какое-то поле 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);
}
person aroyer    schedule 08.08.2012

Контрактное программирование и RTTI показывает, как вы можете использовать dynamic_cast, чтобы разрешить объектам рекламировать какие интерфейсы они реализуют. Мы использовали его в моем магазине, чтобы заменить довольно непрозрачную систему метаобъектов. Теперь мы можем четко описать функциональность объектов, даже если объекты вводятся новым модулем через несколько недель/месяцев после того, как платформа была «запечена» (хотя, конечно, контракты должны быть определены заранее).

person Don Wakefield    schedule 01.08.2012