Избегайте dynamic_cast / RTTI

Недавно я работал над фрагментом кода C++ для побочного проекта ( cpp-markdownбиблиотека для любопытных) и столкнулся с вопросом кодирования, по которому мне хотелось бы высказать свое мнение.

cpp-markdown имеет базовый класс, называемый Token, у которого есть несколько подклассов. Двумя основными подклассами являются Container (которые содержат коллекции других Token s) и TextHolder (используются в качестве базового класса для Token s, которые, конечно, содержат текст).

Большая часть обработки выполняется с помощью виртуальных функций, но некоторые из них лучше обрабатывать с помощью одной функции. Для этого я использовал dynamic_cast преобразование указателя от a Token* к одному из его подклассов, чтобы я мог вызывать функции, специфичные для подкласса и его дочерних классов. Нет никаких шансов, что приведение типов завершится неудачно, потому что код может определить, когда это необходимо, с помощью виртуальных функций (например, isUnmatchedOpenMarker ).

Есть два других способа справиться с этим:

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

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

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

Ответов (6)

Решение

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

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

Просто используйте dynamic_cast<> . Вот для чего он нужен.

Если вы знаете, что преобразование не может быть недействительным, просто используйте static_cast.

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

  • Создайте базовый TokenVisitorкласс, содержащий пустые виртуальные visit(SpecificToken*)методы.
  • Добавьте один виртуальный accept(TokenVisitor*)метод в Token, который вызывает правильно типизированный метод переданного TokenVisitor.
  • Получите от TokenVisitor различные вещи, которые вам нужно будет делать по-разному со всеми токенами.

Для полного шаблона посетителя, полезного для древовидных структур, пусть accept методы по умолчанию перебирают дочерние элементы, вызывающие token->accept(this); каждого из них.

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

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

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

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

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

IMHO, причина того, что dynamic_cast имеет такую ​​репутацию, заключается в том, что его производительность немного ухудшается каждый раз, когда вы добавляете еще один подтип в свою иерархию классов. Если у вас 4-5 классов в иерархии, волноваться не о чем.

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

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