Почему "чистый полиморфизм" предпочтительнее использования RTTI?

Почти каждый ресурс С++, который я видел, который обсуждает это, говорит мне, что я должен предпочесть полиморфные подходы к использованию RTTI (идентификация типа времени выполнения). В общем, я отношусь к этому совету серьезно, и попытаюсь понять его обоснование. В конце концов, С++ - могучий зверь и его трудно понять в полной мере. Однако для этого конкретного вопроса я рисую пробел и хотел бы посмотреть, какие советы может предложить интернет. Во-первых, позвольте мне обобщить то, что я узнал до сих пор, перечисляя общие причины, которые цитируются, почему RTTI "считается вредным":

Некоторые компиляторы не используют его /RTTI не всегда включен

Я действительно не покупаю этот аргумент. Мне нравится говорить, что я не должен использовать возможности С++ 14, потому что там есть компиляторы, которые его не поддерживают. И все же никто не будет препятствовать мне использовать возможности С++ 14. Большинство проектов будут иметь влияние на компилятор, который они используют, и как он настроен. Даже цитируя gcc manpage:

-fno-rtti

Отключить генерацию информации о каждом классе с помощью виртуальных функций для использования функциями идентификации типа времени выполнения С++ (dynamic_cast и typeid). Если вы не используете эти части языка, вы можете сэкономить некоторое пространство, используя этот флаг. Обратите внимание, что исключение обработка использует ту же информацию, но g++ генерирует ее по мере необходимости. Оператор dynamic_cast все еще может использоваться для приведений, которые не требуют информацию типа времени выполнения, то есть отбрасывается на "void *" или на однозначные базовые классы.

Что мне говорит, что если я не использую RTTI, я могу отключить его. Это как сказать, если вы не используете Boost, вам не нужно ссылаться на него. Мне не нужно планировать случай, когда кто-то компилируется с помощью -fno-rtti. Кроме того, компилятор в этом случае не будет громким и ясным.

Он требует дополнительной памяти/может быть медленным

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

Динамический_cast может быть медленным. Однако обычно есть способы избежать использования быстродействующих ситуаций. И я не совсем понимаю альтернативу. Этот ответ SO предлагает использовать перечисление, определенное в базовом классе, для хранения типа. Это работает, только если вы знаете все ваши производные классы априори. Это довольно большое "если"!

Из этого ответа также кажется, что стоимость RTTI также не ясна. Различные люди измеряют разные вещи.

Элегантные полиморфные конструкции сделают RTTI ненужным

Это тот совет, который я воспринимаю всерьез. В этом случае я просто не могу придумать хорошие решения, отличные от RTTI, которые охватывают мой вариант использования RTTI. Позвольте мне привести пример:

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

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};

Теперь мои узлы могут быть разных типов. Как насчет этих:

class red_node : virtual public node_base
{
  public:
    red_node();
    virtual ~red_node();

    void get_redness();
};

class yellow_node : virtual public node_base
{
  public:
    yellow_node();
    virtual ~yellow_node();

    void set_yellowness(int);
};

Черт, почему бы и не один из них:

class orange_node : public red_node, public yellow_node
{
  public:
    orange_node();
    virtual ~orange_node();

    void poke();
    void poke_adjacent_oranges();
};

Последняя функция интересна. Здесь можно написать:

void orange_node::poke_adjacent_oranges()
{
    auto adj_nodes = get_adjacent_nodes();
    foreach(auto node, adj_nodes) {
        // In this case, typeid() and static_cast might be faster
        std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
        if (o_node) {
             o_node->poke();
        }
    }
}

Все это кажется ясным и чистым. Мне не нужно определять атрибуты или методы, в которых они мне не нужны, базовый класс node может оставаться скудным и средним. Без RTTI, с чего начать? Возможно, я могу добавить атрибут node_type в базовый класс:

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();

  private:
    std::string my_type;
};

Является ли std::string хорошей идеей для типа? Может быть, нет, но что еще я могу использовать? Составить число и надеяться, что никто еще не использует его еще? Кроме того, в случае с моим orange_node, что, если я хочу использовать методы из red_node и yellow_node? Должен ли я хранить несколько типов на node? Это кажется сложным.

Заключение

Эти примеры не кажутся чрезмерно сложными или необычными (я работаю над чем-то подобным в моей дневной работе, где узлы представляют собой реальное оборудование, которое контролируется через программное обеспечение, и которые делают совсем другую вещь в зависимости от того, что они представляют). Но я не знаю, как это сделать с помощью шаблонов или других методов. Обратите внимание, что я пытаюсь понять проблему, а не защищать свой пример. Мое чтение страниц, таких как ответ SO, который я связал выше, и эта страница в Wikibooks, кажется, предполагает, что я злоупотребляю RTTI, но я бы хотел чтобы узнать почему.

Итак, вернемся к моему первоначальному вопросу: Почему "чистый полиморфизм" предпочтительнее использования RTTI?

Ответ 1

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

В качестве примера ваш "тыкать соседние апельсины" означает, что я, как сторонник, не могу эмулировать апельсин! Вы конфиденциально объявили оранжевый тип, а затем используйте RTTI, чтобы ваш код работал специальным при взаимодействии с этим типом. Если я хочу "быть оранжевым", я должен быть в вашем частном саду.

Теперь все, кто сочетается с "оранжностью", пары со всем оранжевым типом и неявно со всем своим частным садом, а не с определенным интерфейсом.

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

Это означает, что вашего интерфейса должно быть достаточно, чтобы решить вашу проблему. С этой точки зрения, почему вам нужно только трясти апельсины, и если да, то почему в интерфейсе недоступна орангутация? Если вам нужен нечеткий набор тегов, которые можно добавить ad-hoc, вы можете добавить это к своему типу:

class node_base {
  public:
    bool has_tag(tag_name);

Это обеспечивает аналогичное массивное расширение вашего интерфейса от узко заданного до широкого тега. Кроме того, вместо того, чтобы делать это с помощью RTTI и деталей реализации (ака, "как вы реализовывались?" ), Он делает это с помощью чего-то легко эмулируемого с помощью совершенно другой реализации.

Это может быть даже распространено на динамические методы, если вам это нужно. "Вы поддерживаете Foo'd с аргументами Baz, Tom и Alice? Хорошо, Fooing вы". В большом смысле это менее навязчиво, чем динамическое приведение, чтобы понять, что другой объект является типом, который вам известен.

Теперь объекты мандарина могут иметь оранжевый тег и играть вместе, будучи развязанным по реализации.

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

Абстракция - это игра развязки и скрытия нерелевантности. Это делает код проще рассуждать о локально. RTTI расточает отверстие прямо через абстракцию в детали реализации. Это может облегчить решение проблемы, но у нее есть затраты на блокировку вас в одной конкретной реализации очень легко.

Ответ 2

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

Там, где моралисты терпят неудачу, они предполагают, что все обычаи ошибочны, а на самом деле функции существуют по какой-то причине.

У них есть то, что я называл "сантехническим комплексом": они думают, что все краны неисправны, потому что все краны, которые они вызываются для ремонта. Реальность такова, что большинство кранов работают хорошо: вы просто не называете сантехником для них!

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

Существует общий способ думать о полиморфизме: IF(selection) CALL(something) WITH(parameters). (Извините, но программирование, игнорируя абстракцию, все об этом)

Использование времени моделирования (времени) (времени на основе шаблона), времени выполнения (наследование и на основе виртуальных функций) или данных (RTTI и коммутации) зависит от того, сколько решения известны на каждом из этапов производства и как переменные они находятся в каждом контексте.

Идея такова:

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

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

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

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

Если переключение является многомерным, так как на С++ нет встроенной многократной диспетчеризации, вам необходимо либо:

  • Уменьшить до одного измерения путем Goedelization: там, где виртуальные базы и множественное наследование, с бриллиантами и сложными параллелограммами, но для этого требуется, чтобы количество возможных комбинаций было известно и было относительно небольшим.
  • Объедините измерения один в другой (например, в шаблоне композитных посетителей, но это требует, чтобы все классы знали о своих других братьях и сестрах, поэтому он не может "масштабироваться" из того места, которое оно было задумано).
  • Отправка вызовов на основе нескольких значений. То, для чего RTTI.

Если не только переключение, но даже действия не известны времени компиляции, требуется сценарий и синтаксический анализ: сами данные должны описывать действие, которое необходимо предпринять на них.

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

То, что морализация фактически толкает, чтобы избежать. Но это не означает, что проблем, существующих в донных областях, не существует!

Сбрасывая RTTI только до bash, это как bashing goto только до bash. Вещи для попугаев, а не программисты.

Ответ 3

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

Как насчет dark_orange_node, или black_and_orange_striped_node, или dotted_node? Может ли он иметь точки разных цветов? Что делать, если большинство точек оранжевые, можно ли их вызывать?

И каждый раз, когда вам нужно добавить новое правило, вам придется пересмотреть все функции poke_adjacent и добавить дополнительные if-statements.


Как всегда, трудно создавать общие примеры, я вам это дам.

Но если бы я сделал этот конкретный пример, я бы добавил член poke() ко всем классам и пусть некоторые из них игнорируют вызов (void poke() {}), если они не заинтересованы.

Конечно, это было бы даже менее дорого, чем сравнение typeid s.

Ответ 4

Некоторые компиляторы не используют его /RTTI не всегда включен

Я считаю, что вы неправильно поняли такие аргументы.

Существует ряд кодов С++, где RTTI не используется. Если для принудительного отключения RTTI используются переключатели компилятора. Если вы кодируете такую ​​парадигму... тогда вам почти наверняка уже сообщили об этом ограничении.

Таким образом, проблема связана с библиотеками. То есть, если вы пишете библиотеку, которая зависит от RTTI, то ваша библиотека не может использоваться пользователями, которые отключили RTTI. Если вы хотите, чтобы ваша библиотека использовалась этими людьми, тогда она не может использовать RTTI, даже если ваша библиотека также используется людьми, которые могут использовать RTTI. Не менее важно то, что, если вы не можете использовать RTTI, вам нужно делать покупки немного сложнее для библиотек, так как использование RTTI является для вас разрывом.

Это требует дополнительной памяти/может быть медленным

Есть много вещей, которые вы не делаете в горячих циклах. Вы не выделяете память. Вы не переходите через связанные списки. И так далее. RTTI, безусловно, может быть еще одним из таких вещей "не делайте этого здесь".

Однако рассмотрите все ваши примеры RTTI. Во всех случаях у вас есть один или несколько объектов неопределенного типа, и вы хотите выполнить некоторую операцию над ними, что может быть невозможно для некоторых из них.

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

Изменение сложной операции RTTI в обычном вызове виртуальной функции? Это проблема дизайна. Если вам нужно изменить это, то это то, что требует изменений для каждого производного класса. Это изменяет, как много кода взаимодействует с различными классами. Объем такого изменения выходит далеко за пределы критически важных для кода разделов кода.

Итак... почему вы написали это неправильно, чтобы начать?

Мне не нужно определять атрибуты или методы, в которых они мне не нужны, базовый класс node может оставаться скудным и средним.

В какую сторону?

Вы говорите, что базовый класс "худой и средний". Но на самом деле... это несуществует. Это на самом деле ничего не делает.

Просто посмотрите на свой пример: node_base. Что это? Кажется, это вещь, которая имеет смежные другие вещи. Это Java-интерфейс (в данном случае это пред-генерические Java-классы): класс, который существует исключительно для того, чтобы пользователи могли применять их к реальному типу. Может быть, вы добавите некоторую базовую функцию, например смежность (Java добавляет ToString), но что она.

Есть разница между "худой, средний" и "прозрачный".

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

Но то, что они также делают, - это большая боль, чтобы на самом деле делать новые вещи, даже внутри системы. Рассмотрим вашу функцию poke_adjacent_oranges. Что произойдет, если кто-то хочет тип lime_node, который может быть ткнул точно так же, как orange_node s? Ну, мы не можем получить lime_node из orange_node; это не имеет смысла.

Вместо этого мы должны добавить новый lime_node, полученный из node_base. Затем измените имя poke_adjacent_oranges на poke_adjacent_pokables. Затем попробуйте выполнить кастинг на orange_node и lime_node; какой бы ни был бросок, тот, который мы ткем.

Однако lime_node нуждается в его собственном poke_adjacent_pokables. И эта функция должна выполнять те же проверки кастинга.

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

Очевидно, теперь вы создаете свободную функцию poke_adjacent_pokables, чтобы она работала для всех из них. Но что вы думаете, если кто-то добавляет четвертый тип и забывает добавить его в эту функцию?

Привет, тихий полом. Программа работает более или менее нормально, но это не так. Если бы poke была фактической виртуальной функцией, компилятор потерпел бы неудачу, если бы вы не переопределили чистую виртуальную функцию из node_base.

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

Использование прозрачных базовых классов с RTTI приводит к кошмару обслуживания. Действительно, большинство применений RTTI приводит к головным болям обслуживания. Это не означает, что RTTI не пригодится (например, важно сделать работу boost::any). Но это очень специализированный инструмент для очень специализированных потребностей.

Таким образом, он "вреден" так же, как goto. Это полезный инструмент, который не следует устранять. Но в вашем коде это должно быть редко.


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

Ответ зависит от того, для чего предназначен базовый класс.

Прозрачные базовые классы, такие как node_base, просто используют неправильный инструмент для проблемы. Связанные списки лучше всего обрабатываются с помощью шаблонов. Тип node и смежность будут предоставляться типом шаблона. Если вы хотите поместить полиморфный тип в список, вы можете. Просто используйте BaseClass* как T в аргументе шаблона. Или ваш предпочтительный умный указатель.

Но есть и другие сценарии. Один из них - это тип, который делает много вещей, но имеет некоторые дополнительные части. Конкретный экземпляр может выполнять определенные функции, а другой - нет. Однако дизайн таких типов обычно дает правильный ответ.

Класс "entity" - прекрасный пример этого. Этот класс уже давно страдает от разработчиков игр. Концептуально он имеет гигантский интерфейс, живущий на пересечении почти дюжины совершенно разрозненных систем. И разные объекты имеют разные свойства. Некоторые объекты не имеют визуального представления, поэтому их функции рендеринга ничего не делают. И это все определяется во время выполнения.

Современное решение для этого - система в стиле компонентов. Entity является просто контейнером набора компонентов с некоторым клеем между ними. Некоторые компоненты являются необязательными; объект, который не имеет визуального представления, не имеет "графического" компонента. Объект без ИИ не имеет компонента "контроллера". И так далее.

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

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

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


От Матфея Уолтона:

Я отмечаю, что многие ответы не учитывают идею о том, что ваш пример предполагает, что node_base является частью библиотеки, и пользователи будут создавать свои собственные типы node. Затем они не могут изменить node_base, чтобы разрешить другое решение, поэтому, возможно, RTTI станет их лучшим вариантом.

Хорошо, давайте рассмотрим это.

Чтобы это имело смысл, то, что вам нужно иметь, - это ситуация, когда какая-то библиотека L предоставляет контейнер или другой структурированный держатель данных. Пользователь получает возможность добавлять данные в этот контейнер, перебирать его содержимое и т.д. Однако библиотека не делает ничего с этими данными; он просто управляет своим существованием.

Но он даже не управляет своим существованием так, как его разрушение. Причина в том, что если вы ожидаете использовать RTTI для таких целей, то вы создаете классы, о которых L не знает. Это означает, что ваш код выделяет объект и передает его на L для управления.

Теперь есть случаи, когда что-то вроде этого является законным дизайном. Передача сообщений событий/сообщений, потоковые рабочие очереди и т.д. Общий шаблон здесь таков: кто-то выполняет службу между двумя частями кода, подходящими для любого типа, но служба не должна знать о конкретных видах.

В C этот шаблон пишется void*, и его использование требует большой осторожности, чтобы не сломаться. В С++ этот шаблон пишется std::experimental::any (скоро будет написано std::any).

Способ, которым это должно работать, заключается в том, что L предоставляет класс node_base, который принимает any, который представляет ваши фактические данные. Когда вы получаете сообщение, рабочий элемент очереди потока или что бы вы ни делали, вы затем применяете этот any к соответствующему типу, который знает как отправитель, так и получатель.

Поэтому вместо того, чтобы выводить orange_node из node_data, вы просто вставляете orange внутри поля члена node_data any. Конечный пользователь извлекает его и использует any_cast для преобразования его в orange. Если сбой выполняется, то это не было orange.

Теперь, если вы вообще знакомы с реализацией any, вы, скорее всего, скажете: "Эй, подожди минутку: any внутренне использует RTTI, чтобы сделать работу any_cast". На что я отвечаю: "... да".

Это точка абстракции. В деталях кто-то использует RTTI. Но на уровне, на котором вы должны работать, прямой RTTI - это не то, что вы должны делать.

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

Это называется any. Он использует RTTI, но использование any намного превосходит использование RTTI напрямую, так как оно корректно соответствует желаемой семантике.

Ответ 5

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

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

Есть ли проблема с производительностью? Может быть, но я никогда не испытывал этого, и это могло быть мудростью от двадцати лет назад или от людей, которые искренне верят, что использование трех инструкций по сборке вместо двух - неприемлемое раздувание.

Итак, как с этим справиться... В зависимости от вашей ситуации может иметь смысл иметь любые node -специфические свойства, связанные с отдельными объектами (т.е. весь "оранжевый" API может быть отдельным объектом). Корневой объект может иметь виртуальную функцию, чтобы вернуть "оранжевый" API, возвращая nullptr по умолчанию для неамериканских объектов.

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

Ответ 6

С++ построен на идее проверки статического типа.

[1] RTTI, то есть dynamic_cast и type_id, является проверкой динамического типа.

Итак, по сути, вы спрашиваете, почему статическая проверка типов предпочтительнее проверки динамического типа. И простой ответ: , является ли проверка статического типа предпочтительной для проверки динамического типа, зависит. На много. Но С++ - один из языков программирования, который разработан вокруг идеи проверки статического типа. И это означает, что, например, процесс разработки, в частности тестирование, обычно адаптируется к проверке статического типа, а затем подходит лучше всего.


Re

" Я бы не знал, как это сделать с помощью шаблонов или других методов.

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

#include <iostream>
#include <set>
#include <initializer_list>

namespace graph {
    using std::set;

    class Red_thing;
    class Yellow_thing;
    class Orange_thing;

    struct Callback
    {
        virtual void handle( Red_thing& ) {}
        virtual void handle( Yellow_thing& ) {}
        virtual void handle( Orange_thing& ) {}
    };

    class Node
    {
    private:
        set<Node*> connected_;

    public:
        virtual void call( Callback& cb ) = 0;

        void connect_to( Node* p_other )
        {
            connected_.insert( p_other );
        }

        void call_on_connected( Callback& cb )
        {
            for( auto const p : connected_ ) { p->call( cb ); }
        }

        virtual ~Node(){}
    };

    class Red_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        auto redness() -> int { return 255; }
    };

    class Yellow_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }
    };

    class Orange_thing
        : public Red_thing
        , public Yellow_thing
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        void poke() { std::cout << "Poked!\n"; }

        void poke_connected_orange_things()
        {
            struct Poker: Callback
            {
                void handle( Orange_thing& obj ) override
                {
                    obj.poke();
                }
            } poker;

            call_on_connected( poker );
        }
    };
}  // namespace graph

auto main() -> int
{
    using namespace graph;

    Red_thing   r;
    Yellow_thing    y1, y2;
    Orange_thing    o1, o2, o3;

    for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } )
    {
        o1.connect_to( p );
    }
    o1.poke_connected_orange_things();
}

Это предполагает, что известно множество типов node.

Если это не так, шаблон посетителя (есть много его вариантов) может быть выражен несколькими централизованными отбрасываниями или всего одним.


Для шаблонного подхода см. библиотеку Boost Graph. К сожалению, я не знаком с этим, я не использовал его. Поэтому я не уверен точно, что он делает и как, и в какой степени он использует проверку статического типа вместо RTTI, но поскольку Boost, как правило, основан на шаблонах, поскольку статическая проверка типа является центральной идеей, я думаю, вы обнаружите, что его под-библиотека Graph также основана на проверке статического типа.


[1] Информация о времени выполнения.

Ответ 7

Конечно, есть сценарий, в котором полиморфизм не может помочь: имена. typeid позволяет получить доступ к имени типа, хотя способ кодирования этого имени определяется реализацией. Но обычно это не проблема, так как вы можете сравнить два typeid -s:

if ( typeid(5) == "int" )
    // may be false

if ( typeid(5) == typeid(int) )
   // always true

То же самое относится к хэшам.

[...] RTTI "считается вредным"

вредно, безусловно, завышено: у RTTI есть некоторые недостатки, но у него тоже есть преимущества.

Вам действительно не нужно использовать RTTI. RTTI - это инструмент для решения проблем ООП: следует ли использовать другую парадигму, скорее всего, они исчезнут. C не имеет RTTI, но все еще работает. С++ вместо этого полностью поддерживает OOP и предоставляет вам инструменты multiple для решения некоторых проблем, которые могут потребовать информацию о времени выполнения: один из них действительно RTTI, который, однако, имеет цену. Если вы не можете себе это позволить, то вам лучше заявить только после безопасного анализа эффективности, есть еще старая школа void*: она бесплатна. Безболезненный. Но вы не получаете никакой безопасности. Так что все о сделках.


  • Некоторые компиляторы не используют /RTTI не всегда разрешен
    Я действительно не покупаю этот аргумент. Это похоже на то, что я не должен использовать возможности С++ 14, потому что есть компиляторы, которые не поддерживают его. И все еще, никто не будет препятствовать мне использовать возможности С++ 14.

Если вы пишете (возможно, строго) код С++, вы можете ожидать такого же поведения, независимо от реализации. Стандартно-совместимые реализации должны поддерживать стандартные возможности С++.

Но учтите, что в некоторых средах, которые С++ определяет ( "отдельно стоящие" ), RTTI не обязательно должен предоставляться, а также не делать исключений, virtual и так далее. RTTI нуждается в базовом слое для правильной работы, который занимается деталями низкого уровня, такими как ABI и фактическая информация о типе.


Я согласен с Якком относительно RTTI в этом случае. Да, его можно использовать; но логически ли это? Тот факт, что язык позволяет обойти эту проверку, не означает, что это должно быть сделано.