Кто-нибудь знает, как правильно обрабатывать двойную отправку в C ++ без использования RTTI и dynamic_cast ‹>, а также решение, в котором иерархия классов является расширяемой, то есть базовый класс может быть получен из дальнейшего а его определение / реализация не нужно об этом знать?
Я подозреваю, что выхода нет, но я был бы рад, если бы ошибся :)
Возможность расширения двойной отправки в C ++ без RTTI
Ответы (5)
Первое, что нужно понять, это то, что двойная (или более высокая) отправка не масштабируется. При однократной отправке и n
типах вам понадобится n
функций; для двойной отправки n^2
и так далее. От того, как вы справитесь с этой проблемой, отчасти зависит, как вы справитесь с двойной отправкой. Одним из очевидных решений является ограничение количества производных типов путем создания замкнутой иерархии; в этом случае двойная отправка может быть легко реализована с использованием варианта шаблона посетителя. Если вы не закрываете иерархию, у вас есть несколько возможных подходов.
Если вы настаиваете на том, чтобы каждая пара соответствовала функции, вам в основном понадобится:
std::map<std::pair<std::type_index, std::type_index>, void (*)(Base const& lhs, Base const& rhs)>
dispatchMap;
(При необходимости измените подпись функции.) Вы также должны реализовать функции n^2
и вставить их в dispatchMap
. (Я предполагаю, что вы используете бесплатные функции; нет логической причины помещать их в один из классов, а не в другой.) После этого вы вызываете:
(*dispatchMap[std::make_pair( std::type_index( typeid( obj1 ) ), std::type_index( typeid( obj2 ) )])( obj1, obj2 );
(Вы, очевидно, захотите превратить это в функцию; это не то, что вы хотите разбросать по всему коду.)
Незначительный вариант - сказать, что допустимы только определенные комбинации. В этом случае вы можете использовать find
на dispatchMap
и генерировать ошибку, если не найдете то, что ищете. (Ожидайте много ошибок.) То же решение можно было бы использовать, если бы вы могли определить какое-то поведение по умолчанию.
Если вы хотите сделать это на 100% правильно, с некоторыми функциями, способными обрабатывать промежуточный класс и все его производные, тогда вам понадобится какой-то более динамичный поиск и упорядочение для управления разрешением перегрузки. Рассмотрим, например:
Base
/ \
/ \
I1 I2
/ \ / \
/ \ / \
D1a D1b D2a D2b
Если у вас есть f(I1, D2a)
и f(D1a, I2)
, какой из них следует выбрать. Самое простое решение - это просто линейный поиск, выбор первого, который может быть вызван (как определено dynamic_cast
в указателях на объекты), и ручное управление порядком вставки, чтобы определить желаемое разрешение перегрузки. Однако с n^2
функциями это может довольно быстро замедлиться. Поскольку существует упорядочивание, должно быть возможно использовать std::map
, но функция упорядочивания будет явно нетривиальной для реализации (и все равно придется использовать dynamic_cast
повсюду).
Учитывая все обстоятельства, я предлагаю ограничить двойную отправку небольшими закрытыми иерархиями и придерживаться некоторого варианта шаблона посетителя.
typeid
не подходит.
- person Matthieu M.; 15.06.2011
typeid
и dynamic_cast
). Обратите внимание, что в представленном мною примере LLVM использование isa<>
и cast<>
по-разному поддерживается более слабой формой RTTI. Вместо того, чтобы иметь полную информацию о времени выполнения, вы знаете только, возможно ли приведение.
- person Matthieu M.; 15.06.2011
typeid(type)
(в отличие от typeid(expression)
) и заключив его в виртуальную функцию. RTTI означает, в частности, что на языке C ++ (typeid(expression)
и dynamic_cast
), которое может, например, отключить с помощью -fno-rtti
.
- person Oktalist; 10.07.2016
«шаблон посетителя» в C ++ часто приравнивается к двойной отправке. Он не использует RTTI или dynamic_cast.
См. Также ответы на этот вопрос.
accept
(и для которого посетитель имеет перегрузку visit
). Однако вы можете прекрасно реализовать Child::accept(v)
как { v.visit(*this); Parent::visit(*this); }
.
- person Matthieu M.; 15.06.2011
Первая проблема тривиальна. dynamic_cast
включает две вещи: проверку во время выполнения и приведение типа. Первое требует RTTI, второе - нет. Все, что вам нужно сделать, чтобы заменить dynamic_cast функциональностью, которая делает то же самое, не требуя RTTI, - это иметь собственный метод проверки типа во время выполнения. Для этого все, что вам нужно, - это простая виртуальная функция, которая возвращает некоторую идентификацию того, что это за тип или какой более конкретный интерфейс он соответствует (это может быть перечисление, целочисленный идентификатор или даже строка). Для преобразования вы можете безопасно выполнить static_cast
, как только вы уже выполнили проверку времени выполнения и уверены, что тип, к которому вы выполняете преобразование, находится в иерархии объекта. Таким образом, это решает проблему эмуляции «полной» функциональности dynamic_cast
без необходимости встроенного RTTI. Другое, более сложное решение - создать свою собственную систему RTTI (как это делается в нескольких программах, таких как LLVM, о которой упоминал Матье).
Вторая проблема - большая. Как создать механизм двойной отправки, который хорошо масштабируется с расширяемой иерархией классов. Это сложно. Во время компиляции (статический полиморфизм) это можно сделать довольно хорошо с помощью перегрузок функций (и / или специализаций шаблонов). Во время выполнения это намного сложнее. Насколько мне известно, единственное решение, как упомянул Конрад, - это вести таблицу диспетчеризации указателей функций (или что-то в этом роде). Я считаю, что с некоторым использованием статического полиморфизма и разделением функций диспетчеризации на категории (например, сигнатуры функций и прочее) вы можете избежать нарушения безопасности типов. Но, прежде чем реализовать это, вы должны очень хорошо подумать о своем дизайне, чтобы увидеть, действительно ли эта двойная отправка необходима, действительно ли она должна быть отправкой во время выполнения, и действительно ли она должна иметь отдельную функцию для каждой комбинации задействованы два класса (возможно, вы сможете придумать сокращенное и фиксированное количество абстрактных классов, которые охватывают все действительно различные методы, которые вам нужно реализовать).
Вы можете проверить, как LLVM реализует isa<>
, dyn_cast<>
и cast<>
в качестве системы шаблонов, поскольку он компилируется без RTTI.
Это немного громоздко (требует лакомых кусочков кода в каждом задействованном классе), но очень легковесно.
Руководство программиста LLVM содержит хороший пример и ссылку на реализацию.
(Все 3 метода используют один и тот же лакомый кусочек кода)
Вы можете подделать поведение, реализовав логику множественной отправки во время компиляции самостоятельно. Однако это чрезвычайно утомительно. Бьярн Страуструп является соавтором статьи описывая, как это можно реализовать в компиляторе.
Базовый механизм - таблица диспетчеризации - может быть сгенерирован динамически. Однако, используя этот подход, вы, конечно, потеряете всю синтаксическую поддержку. Вам нужно будет поддерживать двухмерную матрицу указателей методов и вручную искать правильный метод в зависимости от типов аргументов. Это отобразит простой (гипотетический) вызов
collision(foo, bar);
по крайней мере такой же сложный, как
DynamicDispatchTable::lookup(collision_signature, FooClass, BarClass)(foo, bar);
поскольку вы не хотели использовать RTTI. И это при условии, что все ваши методы принимают только два аргумента. Как только требуются дополнительные аргументы (даже если они не являются частью множественной отправки), это становится еще более сложным и потребует обхода безопасности типов.
dynamic_cast
(т.е. избежать необходимости иметьdynamic_cast
для каждого потенциального типа аргумента на всех уровнях, это может быть осуществимо, но если вы хотите избежатьdynamic_cast
любой ценой, тогда вам нужно поддержка с языка, и тогда ответ прост: нельзя. - person David Rodríguez - dribeas   schedule 14.06.2011