Comments 34
Я не совсем понял, зачем прибивать гвоздями классы к их отражению в виде enum - разве что вам очень сильно хочется вынести это в constexpr
В своей реализации я вынес этот "айди" класса в статический член, который генерирует новый айди для каждого класса, и делает это при запуске программы - автоматически, конечно
В итоге всё работает только на виртуальных методах без хранимого объекта айдишника вообще, и при этом очень легко расширяемо - нужно только добавить макрос в класс, примерно как в qt
"На виртуальных методах"... Тогда проще RTTI?
Вы имеете в виду производительность? Нет. RTTI зачастую вообще реализуется через сравнение строк (названий классов), оверхед использования vtable по сравнению с этим - пыль.
Зачем сравнивать строки, чем это поможет? Для dynamic_cast нужно получить указатель на истиное начало класса в памяти, обладая указателем на какой-то базовый класс. И так же знать оффсеты производных классов относительно базового. И эта информация должна быть доступна в момент исполнения. Следовательно, она хранится в каких-то структурах, на которые в конечном счёте имеется ссылка из каждой таблицы виртуальных функций реализуемых данным классом. Как я понимаю. Остальное детали. Возможно в этом алгоритме и есть какая-то итерация по спискам, вызывающая замедление для 100500 раз переунаследованных классов, но уж сравнение строк точно ни за чем и типичном случае там алгоритмическая сложность не зашкаливающая.
Речь скорей о динамических библиотеках, где проверить на совпадение типов через сравнение ссылок на std::type_info нельзя, и приходится сравнивать сами std::type_info по-значению, что заканчивается strcmp. Да, вот она плата за универсальность. В принципе такая-же проблема постигнет любой "самодельный RTTI" как дойдёт до динамических библиотек (нет возможности назначить какой-то числовой идентификатор известный в двух разных библиотеках, например, так как одна о другой может быть вовсе не в курсе).
Но в любом случае, оно делается не какое-то безумное число раз, а как я понимаю, пробегает по списку реализуемых классом таблиц виртуальных функций и для каждой выполняет сравнение. Это единичные вызовы strcmp. И это тоже "пыль". Куда больший эффект дадут промахи кеша и то, что вызываемая из dynamic_cast функция не просматриваются компилятором в момент компиляции, что фактически ломает работу оптимизатора в этой точке.
Да, я имел в виду именно проблему с динамическими библиотеками
Честно говоря, я сам не копал так глубоко, чтобы самостоятельно ковырять все компиляторы и пилить бенчмарки, но читал некоторые статьи, плюс видел, что на каждом моём месте работы (геймдев) была своя похожая замена RTTI, в т.ч. это UE.
Конечно, возможно, что все эти люди и замеры неправы, а рандомный чел из инета - прав, но что то я сомневаюсь =)
Видел способ, похожий на ваш в какой-то статье. Думаю, что между статической переменной и полем enum нет особой разницы, кроме незначительного понижения производительности - к статической перменной все равно придется обращаться на рантайме. В нашем случае мы можем расписать все классы в enum "руками", так что для нас это не проблема. Также, мы можем полностью отказаться от виртуальности.
Есть и другое решение вопроса производительности - кэш. Мы реализовали банальный map по typeid и это стало тоже в разы быстрее. Да, всё ещё требует rtti, зато работает с любыми наследованиями (множественным виртуальным), не требует модификации базового класса и тд. https://github.com/CrazyPandaLimited/panda-lib/blob/master/clib/src/panda/cast.h
Поиск красивых решений для конструкций, которые в чистом коде(да вообще в любом) в принципе не должны использоваться, это по нашему. Может проще реализовывать структуру объектов которая не требует кастовать.
Можно пойти немного дальше и носить в классе не enum, а референс на некую static const структуру описывающую свойства конкретного класса. Но фактически это получается таблица виртуальных функций сделанная вручную. И там могут быть конструкторы копирования, перемещения и т.п., информация и размере/выравнивании класса и т.п.
Такой подход может дать какой-то прирост производительности, т.к. оптимизатор может заглянуть в каждую функцию, если конечно весь код в хедерах, и выбрать сразу нужную и заинлайнить.
Возможно вы боретесь с ветряными мельницами и стоило бы разобраться дальше. Во-первых из ваших же бенчмарков для крупных проектов следует, что избегание dynamic_cast ничего толком особо не ускоряет. А отказ от dynamic_cast ускоряет только ваш конкретный случай. И вполне возможном что дело в самом случае. Или в исключениях, т.к. dynamic_cast подразумевает последние. И непонятно каким образом они обрабатываются компилятором в вашем случае, насколько "бесплатно". Может вовсе и не бесплано.
Или помимо dynamic_cast у вас меняется что-то ещё, например, происходит отказ от виртуальных функций, что существенно влияет на работу оптимизатора (который не может заглянуть в тело виртуальной функции).
Есть ещё пара вариантов. Первый это std::variant с автоматическим kind. Второй - написать один шаблонный visit(Stmt&, Visitor) со switch/function table и повыкидывать все цепочки if...else if.
Дерево на вариантах - это наверное очень весело. Надо будет попробовать. Вообще, по поводу ast_visit был хороший доклад моего коллеги с С++ Russia 2022. К сожалению, он ещё не вышел, но я могу поделиться слайдами. Кстати, про кейс со switch: на сколько я помню, он не очень хорошо с ним дружит, т.к. достать индекс типа в variant стандартными средствами нельзя.
Ну с variant надо накладные считать, экономит указатель и косвенную адресацию но всегда ест max(sizeof). Ну и таблицы в нем обычно если не boost. А switch я имел в виду именно по Stmt, не по variant, простой перебор всех kind: template<Vis> visit(Stmt&, Vis) ... case If: vis(static_cast<If&>)... Чтобы не писать цепочки if dyn_cast, а использовать перегрузки. Например деструктор без virtual: visit(stmt, (auto& s) { delete &s; });
достать индекс типа в variant стандартными средствами нельзя
Можно, конечно! Или речь о каком-то другом variant?
Динамик каст делает больше чем просто енамчик, там сохраняется информация о иерархии. Например с вашей реализацией вы не сможете привести к другой базе, то есть это не точно тот тип, но находящийся в той иерархии
Макросня которую развели абсолютно бесполезна, специализации эти не нужны совершенно, достаточно шаблонной глобальной переменной, инстанс её для каждого типа будет уникальным идентификатором. В случае dll и прочего дерьма можно заменить на просто имя типа(или его уникальный номер внутри как константа написанный, но так легко ошибиться)
И ко всему прочему дизайн где нужно постоянно кастовать динамически ошибочен. Да и даже в такой ситуации как показывают ваши же бенчмарки выигрыш минимален.
То есть это абсолютно бесполезная затея.
Насчёт визитов - внезапно std::visit / variant. Или же ещё лучше(в плане расширяемости и удобства) вариант с стиранием типов
https://github.com/kelbon/AnyAny
Динамик каст делает больше чем просто енамчик, там сохраняется информация о иерархии. Например с вашей реализацией вы не сможете привести к другой базе, то есть это не точно тот тип, но находящийся в той иерархииНа самом деле мало кто задумывается как работает dynamic_cast. С его помощью можно привести не просто к другому типу, который является наследником, а к типу вообще находящемуся во вне иерархии того или иного класса.
Вы это серьезно? Кажется Вы путаете dynamic_cast и reinterpret_cast.
#include <iostream>
#include <memory>
#include <string>
// a.h include
struct a_base {
virtual std::string who() = 0;
virtual ~a_base() = default;
};
struct a : a_base {
std::string who() override { return "a class"; }
};
// b.h include
struct b_base {
virtual std::string who() = 0;
virtual ~b_base() = default;
};
struct b : b_base {
std::string who() override { return "b class"; }
};
// c.h include
struct c : a, b
{
};
// somewhere else
b_base* create_b()
{
return new c;
}
// main
int main()
{
b_base* b = create_b();
std::cout << b->who() << "\n";
a_base* a = dynamic_cast<a_base*>(b);
if (a != nullptr) {
std::cout << "receive a_base from pointer to b_base\n";
std::cout << a->who() << "\n";
} else {
std::cout << "can't receive a_base from pointer to b_base\n";
}
delete b;
b = nullptr;
return 0;
}
Обратите внимание, что иерархия класса a и класса b могут друг о друге ничего не знать, а также оба вообще могут не знать, что в программе есть класс, который наследует их оба. Однако это не мешает dynamic_cast отслеживать эту связь в рантайме и получать через b_base указатель на a_base.В С++ всё это называет cross cast. Идея думаю понятна.
Безусловно, иерархии класса a и класса b могут ничего не знать друг о друге, но о них знает класс c так как наследует и а, и b. А он, в свою очередь, является динамическим типом объекта который находится за указателем b (b_base*) и содержит информацию об обоих иерархиях на основании которой и работает dynamic_cast.
Просто изначальный Ваш посыл воспринимается как-будто эти две иерархии вообще никак не связаны, но это совсем не так для класса c.
(вопросы совместимости RTTI и целесообразности такого подхода на практике уберем за скобки)
Извиняюсь, я сказал статический тип, на самом деле имел ввиду динамический тип объекта на который указывает указатель b со статическим типом b_base.
И не спорю что связывание не является статическим, а лишь говорю, что для независимых иерархий dynamic_cast не работает, что, как мне показалось вы имели ввиду в своем комментарии. Если это не так, то ок.
Нет, там всё правильно написано. Пугает что ваш комментарий кто то лайкнул
Но много ли людей использует виртуальное наследование в 2022 году?
Ну вот в нашем проекте используется и повсеместно, для всех наследований от интерфейсов. (т.к. класс может реализовать несколько интерфейсов, не хочется включать несколько баз, когда эти интерфейсы имеют общего предка).
А что плохого в виртуальном наследовании?
Мы не отрицаем пользу виртуального наследования вообще, если оно вам помогает - это замечательно. Просто мы отказались от него, в пользу более производительного решения. Можем себе позволить)
Есть ли жизнь без RTTI: пишем свой dynamic_cast