Избегайте dynamic_cast / RTTI
Недавно я работал над фрагментом кода C++ для побочного проекта ( cpp-markdown
библиотека для любопытных) и столкнулся с вопросом кодирования, по которому мне хотелось бы высказать свое мнение.
cpp-markdown
имеет базовый класс, называемый Token
, у которого есть несколько подклассов. Двумя основными подклассами являются Container
(которые содержат коллекции других Token
s) и TextHolder
(используются в качестве базового класса для Token
s, которые, конечно, содержат текст).
Большая часть обработки выполняется с помощью виртуальных функций, но некоторые из них лучше обрабатывать с помощью одной функции. Для этого я использовал dynamic_cast
преобразование указателя от a Token*
к одному из его подклассов, чтобы я мог вызывать функции, специфичные для подкласса и его дочерних классов. Нет никаких шансов, что приведение типов завершится неудачно, потому что код может определить, когда это необходимо, с помощью виртуальных функций (например, isUnmatchedOpenMarker
).
Есть два других способа справиться с этим:
Создайте все функции, которые я хочу вызвать как виртуальные функции
Token
, и просто оставьте их с пустым телом для каждого подкласса, кроме одного (ов), который должен их обрабатывать, или ...Создайте виртуальную функцию,
Token
которая будет возвращать правильно типизированный указатель,this
когда он вызывается для определенных подтипов, и нулевой указатель, если вызывается для чего-либо еще. По сути, это расширение системы виртуальных функций, которое я там уже использую.
Второй способ мне кажется лучше, чем существующий и первый. Но хотелось бы узнать мнение других опытных разработчиков C++ по этому поводу. Или я слишком беспокоюсь о мелочах. :-)
Ответов (6)6
# 1 загрязняет пространство имен классов и vtable объектами, которым это не нужно. Хорошо, когда у вас есть несколько методов, которые обычно будут реализованы, но просто уродливы, когда они нужны только для одного производного класса.
№2 просто dynamic_cast<>
в платье в горошек и помаде. Не делает клиентский код проще и запутывает всю иерархию, требуя, чтобы базовый и каждый производный класс были частично осведомлены о каждом другом производном классе.
Просто используйте dynamic_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
.