Pull to refresh

Comments 26

Ни один метод не использует ключевое слово dynamic_cast. Команда разработчиков llvm/clang выбрала не использовать механизм RTTI.

10.2. Самые популярные методы
dyn_cast<X, Y>(Y*)
isa<X,Y>(const Y&)
getType() 


Даааа, в Runtime совсем не используется никакой информации о типах
LLVM не использует RTTI. Вместо него используется оригинальный механизм, и собственные шаблоны dyn_cast<> и isa<>.
Если интересно, могу кинуть ссылку на описание этого механизма.
Да, интересно почитать про отличия, дайте ссылку пожалуйста.
В официальной документации говорится о том, что эти типы работают не только для классов с vtable.

LLVM не использует стандартный RTTI. Но это не означает что они не используют никакого RTTI! Они даже в своих доках так напрямую и пишут:


The LLVM source-base makes extensive use of a custom form of RTTI
Это просто игра словами. Не используется стандартный механизм RTTI языка C++.

Игра словами — это то что у вас получилось.


Самая распространенная претензия к dynamic_cast, та самая "спорность" — "в хорошей архитектуре его просто не должно быть", обычно без обоснования. И про dyn_cast можно сказать то же самое независимо от того считать его RTTI ли нет.

dyn_cast — очень лёгкий и быстрый механизм, с минимальными накладными расходами, в отличие от.
Рассуждения о «хорошей архитектуре» давайте оставим диванным теоретикам.
И что же мешает разработчикам компилятора сделать dynamic_cast таким же хорошим?
То, что реализация dyn_cast требует от программиста дополнительных усилий. Этот механизм не работает автоматически. Вечером выложу сюда ссылку на документ с описанием.
Выше привёл ссылку, там общий принцип механизма изложен.
Могу написать отдельный пост на эту тему.

По той ссылке описана реализация, но совершенно не объяснено, зачем же она нужна. По мне, так описанное там с является самым наивным подходом, легко приводящим к багам. Даже как-то неловко, что выше тут ее называли "оригинальной реализацией".


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

является самым наивным подходом, легко приводящим к багам.

Тем, не менее, этот подход успешно работает.
Так что непонятно, нафига собственная реализация, если для класса с виртуальными функциями компилятор гарантированно может реализовать все оптимальным образом?

Знаете, я прямо сейчас не готов всё сформулировать, зачем это и для чего. Предлагаю вам посмотреть исходники LLVM, там dyn_cast<> и isa<> встречаются повсеместно.
Или я могу написать отдельный пост на эту тему, но это займёт некоторое время.
Тем, не менее, этот подход успешно работает.

Копи-паст тоже успешно работает, но каждая статья о PVS-Studio показывает пачку багов, с ним связанных. Просто удивительно, почему в таком крупном и явно хорошо продуманном проекте используется такое хрупкое решение? Почему нельзя было хотя бы какую-то шаблонную магию применить для генерации уникальных чиселок вместо ручного прописывания enum-ов?


Вообщем, в статье хотелось бы увидеть, аргументированные минусы стандартного dynamic_cast-а и случаи, когда их можно не опасаться, если они есть. По моему наивному предположению для реализации у класса с виртуальными функциями достаточно несколько сравнений указателя на vtable из экземпляра класса со всеми возможными для этого указателя vtable-ами всех классов из иерархии. У компилятора есть вся эта информация, говорить о бинарной совместимости в данном контексте глупо (когда часть классов в другой DLL), с кастомным dynamic_cast-ом ABI тоже не будет. Компилятор для себя всегда может генерировать специальные функции, которыми будет связываться RTTI информация из разных модулей.


Также еще хочется обратить внимание на такой момент: приведенная статья в документации LLVM рекомендует сделать enum со всеми возможными типами классов и сохранить его в поле класса. Если не указывать специально, то размер enum-а должен быть равен размеру int-а, а он, в свою очередь — размеру указателя. Т.е. при использовании встроенных возможностей через dynamic_cast можно сделать виртуальными даже те классы, которым виртуальность не нужна, если вдруг по каким-то причинам их должны dynamic_cast-ить. Накладных расходов по памяти на это не будет, и так и так в класс добавляется дополнительное поле размером с указатель, зато компилятор делает за нас всю работу. А в случае использования с классами, уже имеющими виртуальные функции даже оказываемся в выигрыше.


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

Интересные идеи, над этим стоит поразмышлять. Я не готов сейчас ответить по существу.
Вообщем, в статье хотелось бы увидеть, аргументированные минусы стандартного dynamic_cast-а и случаи, когда их можно не опасаться, если они есть.
У меня есть версия, что они используют более простые (прямые) схемы наследования чем позволяет «настоящий» dynamic_cast и за счет этого более быстрые.
По моему наивному предположению для реализации у класса с виртуальными функциями достаточно несколько сравнений указателя на vtable из экземпляра класса со всеми возможными для этого указателя vtable-ами всех классов из иерархии.
Вы действительно недооцениваете «настоящий» dynamic_cast. Откройте для себя cross_cast:
#include <iostream>

using namespace std;

struct A { virtual ~A(){}; };
struct B { virtual ~B(){}; };
struct C : public A, public B { virtual ~C(){}; };

struct D { virtual ~D(){}; };
struct E { virtual ~E(){}; };
struct F : public D, public E { virtual ~F(){}; }; 

struct G : public C, public F { virtual ~G(){}; };

int main() 
{
	C* ptrС = new G;
        // тут много кода и потом:
	D* ptrD = dynamic_cast<D*>(ptrС);
	
	cout << ptrD;
	
	return 0;
}

Класс «С» без проблем кастируется к классу «D» которого нет в иерархии vtable-ов С. Фактически, для реализации полноценного dynamic_cast, вам придется сравнивать с vtable-ами всех классов известных данному модулю.

Тут как раз не класс «C», а класс «G» кастуется к классу «D», иерархия vtable-ов статического типа кастуемого указателя тут совсем не при чем. И сравнений тут будет не намного больше, чем при ручной реализации инструментарием LLVM (в LLVM может быть сделана оптимизация, заключающаяся в проверке диапазонов, экономящая сравнения. Но мне кажется, это экономия на спичках).


Здесь при касте затребован тип «D», значит компилятор должен сделать проверки только с vtable-ами всех типов, которые наследуются от него и с ним самим. В частности, здесь будет максимум всего 3 сравнения: ptrC->vtbl с таблицами классов «D», «F» и «G». Пожалуй, компилятор даже оптимизация с диапазонами может сделать, если будет располагать vtable классов в порядке обхода root-childs их дерева наследования, как предлагают делать в той статье в документации LLVM со значениями перечислений. Впрочем, в данном случае это сэкономит лишь одно сравнение, так что овчинка не стоит выделки.

Ну все верно. Только представьте, что в общем виде, может быть какой-нибудь класс который наследует все остальные — допустим «Z», и в данном дереве vtable-ов любой класс может быть скастирован к любому другому. Если сюда еще добавить возможность виртуального наследования (это ересь, на мой взгляд, но все же), то механизм становиться не таким уж тривиальным. Если данный механизм используется очень активно, то это может сильно сказаться на скорости.
В моей практике был случай, когда классы на стеке создавались так часто, что переопределив конструктор по-умолчанию (который зануляет поля класса) мы ускорили код в 10 раз, а что говорить про dynamic_cast.

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


Вот я и кастую статью, где бы все это было объяснено. Тем более сейчас с новым стандартом, где есть final классы, дерево классов можно ограничить.

Ну согласитесь, это редкие случаи, а для частных случаев почему-бы компилятору не делать более быстрый код?
Согласен. Я и не утверждаю что частный случай нельзя сделать быстрее. Скорее наоборот, разработчики LLVM в своем частном случае, как раз, сделали свою частную быструю реализацию.
И если они решаются вручную, пусть для каких-то частных случаев, что мешает компилятору распознать эти частные случаи и сделать все оптимально?
Я не являюсь специалистом по компиляторам и могу только предположить. Что мешает компилятору делать очевидные для человека оптимизации (инлаининг, убирать лишнюю загрузку из памяти, выкидывать не нужные методы и т.д.)? — недостаток глобальной информации. Просто представьте что разработчики компилятора решают комплексную проблему, на все случаи жизни. Dynamic_cast, как бы, подсказывает нам, что статической информации недостаточно. Представьте что вы сделали такою частную оптимизацию в либе… и вдруг, вы линкуете стороннюю либу где появляется такой монстр как я показал выше. Или Dll (плагин например)? Теперь dynamic_cast не может пользоваться оптимизацией, а должен переключиться на общий случай.
Я думаю что если такие оптимизации возможны, они бы были, либо в новых стандартах нужно будет вводить дополнительные ограничения (типа ABI и т.п.).

Я бы еще перефразировал фразу


Почти все глобальные функции объявлены как статические.

"Около половины" не совсем то же самое, что и "почти все".

очень хорошо спроектирован

Быдлокодеров туда тоже иногда пускают. Два года назад где то в libclang добавились функции для вычисления выражений (clang_Cursor_Evaluate и.т.п.). Но функция выдающая целочисленный результат возвращала только 32 битное число, при том что весь внутренний код изначально вполне вменяемо вычислял 64-битные значения — оно просто потом тупо обрезалось. Как же бесило то что кто то в 201х годах еще не знает о том что бывают 64 битные вычисления. Правил в своем локальном форке пока в новой версии они не добавили таки потом костыль clang_EvalResult_getAsLongLong, но почему сразу было не сделать нормально большой вопрос.
Интересно. Я туда не заглядывал.
Имхо, здесь имеется в виду вот что. LLVM основан на мощной концепции: языке LLVM IR, и системе классов, представляющих основные структуры компилятора. Именно эти базовые вещи спроектированы очень хорошо. А 100500 проходов оптимизации и трансформации кода могут быть написаны самыми разными людьми, и не все из них гении, увы. Но хорошая базовая структура позволяет развивать и поддерживать такой большой проект.
Ещё один урок. Все допускают ошибки. И разработчики компиляторов тоже. Подстраховывайтесь, используя инструменты статического анализа кода. Например, тот же Clang Static Analyzer. Или что-то помощнее :). Находим баги в LLVM 8 с помощью анализатора PVS-Studio.

Я с вами полностью согласен, всегда с интересом читаю ваши статьи.

Sign up to leave a comment.

Articles