Возможность расширения двойной отправки в C ++ без RTTI

Кто-нибудь знает, как правильно обрабатывать двойную отправку в C ++ без использования RTTI и dynamic_cast ‹>, а также решение, в котором иерархия классов является расширяемой, то есть базовый класс может быть получен из дальнейшего а его определение / реализация не нужно об этом знать?
Я подозреваю, что выхода нет, но я был бы рад, если бы ошибся :)


person George Penn.    schedule 14.06.2011    source источник
comment
Подходит ли вам двойная отправка во время компиляции? (Полагаю, что нет, но на всякий случай есть простое решение.)   -  person Konrad Rudolph    schedule 14.06.2011
comment
Это не так, поскольку он не допускает расширяемости (я пытаюсь написать библиотеку, а не только предопределенный набор классов).   -  person George Penn.    schedule 14.06.2011
comment
@ Конрад, из любопытства, ты думаешь о CRTP? И если да, то @George есть причина, по которой это нежизнеспособно?   -  person Nim    schedule 14.06.2011
comment
Потому что мне нужен динамический полиморфизм. В противном случае это было бы вполне жизнеспособно.   -  person George Penn.    schedule 14.06.2011
comment
@Nim Нет. Просто перегрузка (и подкласс шаблонов, который используется для создания иерархии наследования во время компиляции, и в некоторых отношениях она похожа на CRTP).   -  person Konrad Rudolph    schedule 14.06.2011
comment
Что вы хотите не использовать в RTTI? Поскольку мы говорим о динамической отправке, RTTI является требованием. Если вы хотите минимизировать количество ручных dynamic_cast (т.е. избежать необходимости иметь dynamic_cast для каждого потенциального типа аргумента на всех уровнях, это может быть осуществимо, но если вы хотите избежать dynamic_cast любой ценой, тогда вам нужно поддержка с языка, и тогда ответ прост: нельзя.   -  person David Rodríguez - dribeas    schedule 14.06.2011


Ответы (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 повсюду).

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

person James Kanze    schedule 14.06.2011
comment
@James: поскольку OP упоминал без RTTI, я боюсь, что решение на основе typeid не подходит. - person Matthieu M.; 15.06.2011
comment
@Matthieu Я думал, он хочет отправить по типу. В этом случае без RTTI возникает противоречие: вы не можете выполнять диспетчеризацию в соответствии с типом во время выполнения, не определяя тип во время выполнения. - person James Kanze; 15.06.2011
comment
@ Джеймс: есть другие способы узнать тип, кроме RTTI. Например, LLVM создал довольно умную (и, что немаловажно) альтернативу. - person Matthieu M.; 15.06.2011
comment
По сути, я хотел исключить RTTI из-за плохой поддержки компилятора и из-за того, что все говорят, что вам не нужен RTTI во встроенной среде. Однако переменная, указывающая тип, является жизнеспособным вариантом. В конце концов, vtable по сути делает это определенным образом. - person George Penn.; 15.06.2011
comment
@Matthieu Если вы знаете тип во время выполнения, это RTTI. По определению. Вы можете использовать RTTI, предоставляемый компилятором C ++, или реализовать свой собственный; для конкретных случаев (не требующих полной универсальности) то, что вы реализуете, может быть намного быстрее или меньше, чем то, что делает компилятор, но это все равно RTTI. - person James Kanze; 15.06.2011
comment
@ Джеймс: Кажется, мы используем одно и то же слово для разных целей. Для меня RTTI конкретно относится к стандартной реализации C ++ информации о типе времени выполнения (используется для typeid и dynamic_cast). Обратите внимание, что в представленном мною примере LLVM использование isa<> и cast<> по-разному поддерживается более слабой формой RTTI. Вместо того, чтобы иметь полную информацию о времени выполнения, вы знаете только, возможно ли приведение. - person Matthieu M.; 15.06.2011
comment
@ Матье Да. Я использую этот термин в его повседневном смысле. Насколько я знаю, у него нет особого смысла в C ++. (Я не могу найти этот термин в стандарте.) Все, что предоставляет информацию о типе во время выполнения, является RTTI. (Я использовал этот термин до того, как С ++ получил его поддержку, например, для описания того, что мы реализовали.) - person James Kanze; 15.06.2011
comment
Вы можете легко изменить этот ответ для работы без RTTI, используя typeid(type) (в отличие от typeid(expression)) и заключив его в виртуальную функцию. RTTI означает, в частности, что на языке C ++ (typeid(expression) и dynamic_cast), которое может, например, отключить с помощью -fno-rtti. - person Oktalist; 10.07.2016

«шаблон посетителя» в C ++ часто приравнивается к двойной отправке. Он не использует RTTI или dynamic_cast.

См. Также ответы на этот вопрос.

person Joris Timmermans    schedule 14.06.2011
comment
Хорошо, но тогда Посетитель должен знать всю иерархию классов, что тоже недопустимо. Нет другого выхода? - person George Penn.; 14.06.2011
comment
Кроме того, шаблон «Посетитель» должен посещать только листы в иерархии классов. - person George Penn.; 14.06.2011
comment
@ Джордж Пенн: он может посещать другие вещи, которые уходят. - person Matthieu M.; 14.06.2011
comment
@Matthieu Ага, я только что проверил. Виноват. - person George Penn.; 14.06.2011
comment
@Matthieu На самом деле, я думаю, что посетителю было бы сложно посетить и родительский, и дочерний классы, поскольку это был бы неприятный тип перегрузки, который не допускает наследование, и абстрактный Visitor должен быть производным. Если я ошибаюсь, не могли бы вы уточнить? - person George Penn.; 14.06.2011
comment
@ Джордж Пенн: Вы правы в том, что по умолчанию посетитель перейдет к самому производному классу, который перегружает метод 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, о которой упоминал Матье).

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

person Mikael Persson    schedule 14.06.2011

Вы можете проверить, как LLVM реализует isa<>, dyn_cast<> и cast<> в качестве системы шаблонов, поскольку он компилируется без RTTI.

Это немного громоздко (требует лакомых кусочков кода в каждом задействованном классе), но очень легковесно.

Руководство программиста LLVM содержит хороший пример и ссылку на реализацию.

(Все 3 метода используют один и тот же лакомый кусочек кода)

person Matthieu M.    schedule 14.06.2011

Вы можете подделать поведение, реализовав логику множественной отправки во время компиляции самостоятельно. Однако это чрезвычайно утомительно. Бьярн Страуструп является соавтором статьи описывая, как это можно реализовать в компиляторе.

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

collision(foo, bar);

по крайней мере такой же сложный, как

DynamicDispatchTable::lookup(collision_signature, FooClass, BarClass)(foo, bar);

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

person Konrad Rudolph    schedule 14.06.2011
comment
Спасибо, это ответ, однако ответ Джеймса более подробный, поэтому я принимаю его. - person George Penn.; 15.06.2011