В современном С++ осталось не так много вещей, которые не подходят под парадигму "Не плати за то, что не используешь". Одна из них – dynamic_cast. В рамках данной статьи мы разберёмся, что с ним не так, а когда поймём – попробуем предложить альтернативу.

Об исследуемой области
Давайте вспомним немного азов языка C++. Если вам покажется скучной эта часть, вы всегда можете перейти к следующей главе.
Итак, представим, что у нас есть следующая иерархия классов:
struct Shape { virtual void Draw() const = 0; }; struct Circle : Shape { void Draw() const override; }; struct Triangle : Shape { void Draw() const override; }; struct Rectangle : Shape { void Draw() const override; };
Согласно объектно-ориентированной парадигме мы без каких-либо проблем можем производить "расширяющие" преобразования (upcasting) из указателя на объект дочернего класса к указателю на объект базового класса:
#include <memory> void foo(const Shape *); void bar() { std::unique_ptr<Shape> circle = std::make_unique<Circle>(); std::unique_ptr<Shape> triangle = std::make_unique<Triangle>(); std::unique_ptr<Shape> rect = std::make_unique<Rectangle>(); foo(circle.get()); foo(triangle.get()); foo(rect.get()); }
Т.к. производные классы Circle, Triangle и Rectangle гарантированно содержат в себе инстанс базового класс Shape, компилятор без каких-либо накладных расходов может сделать такое преобразование.
А что, если мы теперь захотим преобразовать указатель на базовый класс в указатель на производный? Под указателем на Shape может скрываться любой производный от него класс. Нужно как-то на этапе исполнения программы понять, что же в реальности хранится под указателем. Тут на сцену и выходит механизм динамической идентификации типа данных (runtime type identification, сокр. RTTI).
RTTI – это специальный механизм, который позволяет определить тип данных переменной во время выполнения программы. Механизм RTTI применяется всякий раз, когда вы используете операторы dynamic_cast и typeid. Стандарт C++ не определяет, как именно реализуется RTTI, и вся ответственность перекладывается на двоичный интерфейс приложений (application binary interface, сокр. ABI). В большинстве компиляторов информация хранится в виртуальной таблице (vtable). Если вам интересны детали, то в качестве одного из примеров можно ознакомиться с имплементацией на платформе x86_64.
Чтобы корректно произвести "сужающие" преобразования (downcasting) из указателя на объект базового класса к указателю на объект дочернего класса, воспользуемся оператором dynamic_cast:
void Visit(const Shape *ptr) { if (auto circle = dynamic_cast<const Circle *>(ptr)) { // do smth with circle } else if (auto triangle = dynamic_cast<const Triangle *>(ptr)) { // do smth with triangle } else if (auto rect = dynamic_cast<const Rectangle *>(ptr)) { // do smth with rect } }
Конечно, dynamic_cast в пользовательском коде встречается не так часто, typeid используют еще реже. Кто-то считает, что использование таких операторов – симптом плохого дизайна приложения. Однако, как обстоят дела в приложениях, в которых такие операции – вынужденная мера, и их число может измеряться миллионами? Настолько ли это медленно – слазить в vtable? Об этом мы и поговорим.
Проблематика
Статические анализаторы, как и компиляторы, работают с исходным кодом через его промежуточное представление. Одно из них – абстрактное синтаксическое дерево (AST). В нём узлы выступают в роли различных языковых конструкций.
Удобный способ реализовать AST в коде – иерархия классов. Например, у компилятора Clang узлы дерева наследуются от таких классов, как Decl (интерфейс для объявлений функций и типов) и Stmt (интерфейс для конструкций).
Рассмотрим пример. Допустим у нас есть следующий синтетический блок кода:
{ for (size_t i = 0; i < 10; ++i) ; if (true) ; }
Согласно грамматикам языков C и C++, это составная конструкция (блок), содержащая цикл for и ветвление if. Если попробовать реализовать это в объектно-ориентированной парадигме, у нас будут следующие сущности:
// Base class for all type of statements struct Stmt { /* .... */ }; // Base class for all type of expressions struct Expr { /* .... */ }; // Base class for all types of declarations struct Decl { /* .... */ }; struct IfStmt : Stmt { const Expr& GetCondition() const noexcept; const Stmt& GetBody() const noexcept; const Stmt* GetElse() const noexcept; // .... }; struct ForStmt : Stmt { const Decl* GetInit() const noexcept; const Expr* GetCondition() const noexcept; const Expr* GetPostBodyExpr() const noexcept; const Stmt& GetBody() const noexcept; // .... }; using StatementList = ....; struct CompoundStmt : Stmt { auto begin() const noexcept { return stmts.begin(); } auto end() const noexcept { return stmts.end(); } private: StatementList stmts; // .... };
Мы будем класть все конструкции внутри блока в некоторый контейнер, например std::vector. Так как возможных конструкций в языках не одна и даже не две, будем работать с ними через указатели на базовый класс Stmt. Теперь мы можем перебрать все конструкции:
void foo(const CompoundStmt *compStmt) { for (auto stmt : compStmt) { // do smth } }
И последнее, что мы хотим сделать, это позвать для каждого специализированного узла свою кастомную логику:
void Visit(const IfStmt &); void Visit(const ForStmt &);
И здесь мы как раз сталкиваемся с проблемой – необходимо узнать на этапе работы программы, какая именно конструкция лежит под указателем на Stmt и позвать нужную перегрузку:
void foo(const Stmt *stmt) { for (auto stmt : compStmt) { if (auto ifStmt = dynamic_cast<const IfStmt *>(stmt)) { Visit(*ifStmt); } else if (auto forStmt = dynamic_cast<const ForStmt *>(stmt)) { Visit(*forStmt); } } }
Однако есть маленькая деталь, из-за которой код выше может не скомпилироваться. Если базовый класс Stmt не полиморфен, то магия dynamic_cast работать не будет. Чтобы исправить проблему, нужно добавить хотя бы одну виртуальную функцию. Например, так:
struct Stmt { virtual void Dummy(); /* .... */ };
Теперь всё хорошо и код будет работать в большинстве случаев. Пока не окажется, что в проекте по разным объективным причинам выключен RTTI. Например, в компиляторах GCC/Clang это можно сделать, передав флаг -fno-rtti (GCC, Clang), в MSVC – посредством /GR-. Стоит отметить, что выключение RTTI не задевает механизм исключений и динамическую диспетчеризацию.
Кстати, пока готовил статью, нашёл интересный опрос на isocpp.org. В нём из 2058 респондентов 14% ответили, что они частично отключают RTTI на своих программах, а 18% отключают его полностью.
Подытожим, dynamic_cast плох тем, что:
Работает только на полиморфных типах.
Не работает с флагом -fno-rtti.
Для работы требуется ABI-специфичная информация о типах.
"Сужающие" преобразования работают через просмотр vtable, а это медленная операция.
Одним из решений проблемы может быть использование паттерна проектирования "посетитель" (визитор). Оно избавит от необходимости производить dynamic_cast ценой одного-двух вызовов виртуальной функции. Однако давайте посмотрим, как можно поступить иначе.
Пишем свой dynamic_cast
Это база
Рецепт лечения на самом деле прост. Мы внедрим в родительский класс поле с информацией о типе и будем сами проверять её. Это может выглядеть так:
struct Stmt { enum Type { IfStmt, ForStmt, DoStmt, WhileStmt, .... }; const Type m_kind; Stmt(Type kind) : m_kind { kind } { /* .... */ } /* .... */ }; struct IfStmt : Stmt { IfStmt() : Stmt { Stmt::Type::IfStmt } { /* .... */ } /* .... */ }; struct ForStmt : Stmt { ForStmt() : Stmt { Stmt::Type::ForStmt } { /* .... */ } /* .... */ };
Тогда при создании объекта дочернего класса мы записываем его тип в поле m_kind. Теперь мы можем проверять тип следующим образом:
void foo(const Stmt *stmt) { if (stmt->m_kind == Stmt::Type::IfStmt) { auto ifStmt = static_cast<const IfStmt *>(stmt) Visit(*ifStmt); } else if (stmt->m_kind == Stmt::Type::ForStmt) { auto forStmt = static_cast<const ForStmt *>(stmt) Visit(*forStmt); } }
Из плюсов данного подхода можно отметить:
Полиморфность классов теперь необязательна.
Код может компилироваться без RTTI.
Мы сами контролируем размер информации о типе. У класса известны накладные расходы – это размер перечисления.
Быстрая проверка и преобразование через static_cast в теории быстрее, чем преобразование через dynamic_cast.
Однако, стоит отметить и минусы:
Мы пишем больше кода.
При множественном наследовании логика проверок будет не такой простой.
static_cast не умеет работать с виртуальным наследованием и код не скомпилируется. Но много ли людей использует виртуальное наследование в 2022 году?
Посмотрев на код, можно задаться вопросом "А где здесь аналог dynamic_cast"? Конечно, писать проверки именно таким образом не очень удобно. Чтоб исправить это, добавим немного абстракций. Рассмотрим, как это реализовано у нас в PVS-Studio и в LLVM.
Наводим красоту. Подход в PVS-Studio
Вернёмся к нашему примеру:
struct Stmt { enum Type { IfStmt, ForStmt, DoStmt, WhileStmt, .... }; Stmt(Type kind) : m_kind { kind } { /* .... */ } Type Kind() const { return m_kind; } private: const Type m_kind; /* .... */ };
Реализуем шаблон функции IsA, который будет принимать указатель наш объект и предполагаемый тип. Внутри она будет выполнять проверку на нулевой указатель и на нужный kind.
template <typename T, typename Kind> bool IsA(T *p, Kind kind) noexcept { return p && p->Kind() == kind; }
Далее реализуем пару шаблонов структур без определения:
template <Stmt::Type K> struct type_from_kind; template <typename T> struct kind_from_type;
Они нужны для того, чтобы потом специализировать их для каждого производного класса. Таким образом, мы свяжем тип узла дерева и константу перечисления, находящуюся внутри класса.
template <> struct type_from_kind<Stmt::IfStmt> { using type = IfStmt; }; template <> struct kind_from_type<IfStmt> { static constexpr auto value = Stmt::Type::IfStmt; };
Конечно, писать такие специализации для каждого класса не очень приятно. Тут нам на помощь может прийти "макросная магия":
#define MAKE_STMT_TRAITS(t, k) \ template <> struct type_from_kind<k> \ { \ using type = t; \ }; \ template <> struct kind_from_type<t> \ { \ static constexpr auto value = k; \ };
Тогда определение дочерних классов будет выглядеть так:
struct IfStmt : Stmt { /* .... */ }; MAKE_STMT_TRAITS(IfStmt, Stmt::IfStmt) struct ForStmt : Stmt { /* .... */ }; MAKE_STMT_TRAITS(ForStmt, Stmt::ForStmt)
Теперь можно реализовать свой dyn_cast, инкапсулирующий static_cast c проверкой на нужный тип через функцию IsA:
template <typename To, typename From> requires std::is_pointer_v<To> && std::convertible_to<From, To> auto dyn_cast(From *p) noexcept { using ResultType = std::remove_cvref_t<std::remove_pointer_t<To>>; return IsA(p, kind_from_type<ResultType>::value) ? static_cast<To>(p) : nullptr; }
Благодаря такому подходу можно добиться согласованности со способом, когда мы просто писали dynamic_cast:
void foo(const Stmt *stmt) { for (auto stmt : compStmt) { if (auto ifStmt = dyn_cast<const IfStmt *>(stmt)) { Visit(*ifStmt); } else if (auto forStmt = dyn_cast<const ForStmt *>(stmt)) { Visit(*forStmt); } } }
Теперь рассмотрим альтернативный способ.
Наводим красоту. Подход в LLVM
Вернёмся к нашей структуре Stmt:
struct Stmt { enum Type { IfStmt, ForStmt, DoStmt, WhileStmt, .... }; Stmt(Type kind) : m_kind { kind } { /* .... */ } Type Kind() const { return m_kind; } private: const Type m_kind; /* .... */ };
В каждый класс-наследник мы внедряем статическую функцию-член classof. Она принимает указатель на базовый класс и проверяет его на совпадение с kind дочернего класса:
struct IfStmt : Stmt { static bool classof(const Stmt *stmt) noexcept { return stmt->Kind() == Stmt::Type::IfStmt; } /* .... */ }; struct ForStmt : Stmt { static bool classof(const Stmt *stmt) noexcept { return stmt->Kind() == Stmt::Type::ForStmt; } /* .... */ };
Далее, в функции IsA нужно просто позвать classof из типа, который нам передали первым шаблонным параметром:
template <typename To, typename From> bool IsA(From *p) noexcept { using ResultType = std::remove_cvref_t< std::remove_pointer_t< std::remove_cv_ref_t<To> > >; return ResultType::classof(p); }
Тогда наш dyn_cast будет выполнять такую же проверку IsA, как и в первом способе, и в зависимости от того, совпал тип или нет, делать static_cast к нужному типу или возвращать нулевой указатель:
template <typename To, typename From> requires std::is_pointer_v<To> && std::convertible_to<From *, To> auto dyn_cast(From *p) noexcept { using res_type = std::remove_cv_t < std::remove_pointer_t< std::remove_cvref_t<To> > >; return IsA<res_type>(p) ? static_cast<To>(p) : nullptr; }
Бенчмарки
Когда я рассказывал про бенчмарки на конференции, я привёл только синтетические тесты. Было подозрение, что этого недостаточно, это же отметил и один из коллег. Был разговор о том, что, не зная, сколько именно производительности даёт описанный способ, не стоит переписывать на него код. В этой статье я исправляюсь и привожу статистику, на сколько замедлится ядро анализатора, если вернуть dynamic_cast.
Синтетический пример
Синтетический бенчмарк выглядит следующим образом (ссылка на Quick C++ Benchmark):
Определяем структуру Stmt_RTTI, от которой будут наследоваться структуры IfStmt_RTTI и ForStmt_RTTI. Их мы впоследствии будем преобразовывать при помощи dynamic_cast.
Определяем структуру Stmt_WithEnum, от которого будут наследоваться IfStmt_WithEnum и ForStmt_WithEnum. Их мы будем преобразовывать при помощи проверки функцией IsA, реализованной двумя вышеописанными способами, и преобразованием при помощи static_cast.
Формируем вектор из 1'000'000 умных указателей на тип Stmt_RTTI / Stmt_WithEnum, инициализируем их псевдослучайно наследниками.
Итерируемся по вектору и делаем преобразование к одному из дочерних типов.
Код бенчмарка:
#include <memory> #include <vector> #include <random> struct Stmt_RTTI { virtual ~Stmt_RTTI() = default; }; struct IfStmt_RTTI : Stmt_RTTI { }; struct ForStmt_RTTI : Stmt_RTTI { }; struct Stmt_WithEnum { enum Type { IfStmt, ForStmt, DoStmt, WhileStmt }; Stmt_WithEnum(Type kind) : m_kind { kind } { } virtual ~Stmt_WithEnum() = default; Type Kind() const { return m_kind; } private: const Type m_kind; }; namespace Solution1 { template <Stmt_WithEnum::Type K> struct type_from_kind; template <typename T> struct kind_from_type; #define MAKE_STMT_TRAITS(t, k) \ template <> struct Solution1::type_from_kind<k> \ { \ using type = t; \ }; \ template <> struct Solution1::kind_from_type<t> \ { \ static constexpr auto value = k; \ }; template <typename T, typename Kind, typename ...Kinds> bool IsA(T stmt, Kind kind, Kinds ...kinds) noexcept { return stmt && ((stmt->Kind() == kind) || ... || (stmt->Kind() == kinds)); } template <typename To, typename From> requires std::is_pointer_v<To> && requires { static_cast<To>(std::declval<From *>()); } auto dyn_cast(From *p) noexcept { using ResultType = std::remove_cvref_t< std::remove_pointer_t< std::remove_cvref_t<To> > >; return IsA(p, kind_from_type<ResultType>::value) ? static_cast<To>(p) : nullptr; } } struct IfStmt_WithEnum : Stmt_WithEnum { IfStmt_WithEnum() : Stmt_WithEnum { Stmt_WithEnum::IfStmt } {} static bool classof(const Stmt_WithEnum *p) noexcept { return p && p->Kind() == Stmt_WithEnum::IfStmt; } }; MAKE_STMT_TRAITS(IfStmt_WithEnum, Stmt_WithEnum::IfStmt) struct ForStmt_WithEnum : Stmt_WithEnum { ForStmt_WithEnum() : Stmt_WithEnum { Stmt_WithEnum::ForStmt } {} static bool classof(const Stmt_WithEnum *p) noexcept { return p && p->Kind() == Stmt_WithEnum::ForStmt; } }; MAKE_STMT_TRAITS(ForStmt_WithEnum, Stmt_WithEnum::ForStmt) namespace Solution2 { template <typename To, typename From> bool IsA(From *p) noexcept { using ResultType = std::remove_cvref_t< std::remove_pointer_t< std::remove_cvref_t<To> > >; return ResultType::classof(p); } template <typename To, typename From> requires std::is_pointer_v<To> && requires { static_cast<To>(std::declval<From *>()); } auto dyn_cast(From *p) noexcept { using ResultType = std::remove_cvref_t< std::remove_pointer_t< std::remove_cvref_t<To> > >; return IsA<ResultType>(p) ? static_cast<To>(p) : nullptr; } } std::unique_ptr<Stmt_RTTI> factory_1() { static std::mt19937_64 Generator { 0 }; std::uniform_int_distribution d { 0, 1 }; switch (d(Generator)) { case 0: return std::make_unique<IfStmt_RTTI>(); case 1: return std::make_unique<ForStmt_RTTI>(); } std::terminate(); } std::unique_ptr<Stmt_WithEnum> factory_2() { static std::mt19937_64 Generator { 0 }; std::uniform_int_distribution d { 0, 1 }; switch (d(Generator)) { case 0: return std::make_unique<IfStmt_WithEnum>(); case 1: return std::make_unique<ForStmt_WithEnum>(); } std::terminate(); } static void StmtRTTI_Benchmark(benchmark::State& state) { std::vector<std::unique_ptr<Stmt_RTTI>> vec; const auto size = 1'000'000u; vec.reserve(size); for (size_t i = 0; i < size; ++i) { vec.push_back(factory_1()); } // Code inside this loop is measured repeatedly for (auto _ : state) { for (const auto &stmt : vec) { if (auto ifStmt = dynamic_cast<const IfStmt_RTTI *>(stmt.get())) { // Make sure the variable is not optimized away by compiler benchmark::DoNotOptimize(ifStmt); } else if (auto forStmt = dynamic_cast<const ForStmt_RTTI *>(stmt.get())) { // Make sure the variable is not optimized away by compiler benchmark::DoNotOptimize(forStmt); } } } } // Register the function as a benchmark BENCHMARK(StmtRTTI_Benchmark); static void StmtWithEnum_Benchmark_1(benchmark::State& state) { std::vector<std::unique_ptr<Stmt_WithEnum>> vec; const auto size = 1'000'000u; vec.reserve(size); for (size_t i = 0; i < size; ++i) { vec.push_back(factory_2()); } // Code inside this loop is measured repeatedly for (auto _ : state) { for (const auto &stmt : vec) { if (auto ifStmt = Solution1::dyn_cast<const IfStmt_WithEnum *>(stmt.get())) { // Make sure the variable is not optimized away by compiler benchmark::DoNotOptimize(ifStmt); } else if (auto forStmt = Solution1::dyn_cast<const ForStmt_WithEnum *>(stmt.get())) { // Make sure the variable is not optimized away by compiler benchmark::DoNotOptimize(forStmt); } } } } BENCHMARK(StmtWithEnum_Benchmark_1); static void StmtWithEnum_Benchmark_2(benchmark::State& state) { std::vector<std::unique_ptr<Stmt_WithEnum>> vec; const auto size = 1'000'000u; vec.reserve(size); for (size_t i = 0; i < size; ++i) { vec.push_back(factory_2()); } // Code inside this loop is measured repeatedly for (auto _ : state) { for (const auto &stmt : vec) { if (auto ifStmt = Solution2::dyn_cast< const IfStmt_WithEnum *>(stmt.get())) { // Make sure the variable is not optimized away by compiler benchmark::DoNotOptimize(ifStmt); } else if (auto forStmt = Solution2::dyn_cast<const ForStmt_WithEnum *>(stmt.get())) { // Make sure the variable is not optimized away by compiler benchmark::DoNotOptimize(forStmt); } } } } BENCHMARK(StmtWithEnum_Benchmark_2);
Результаты:

Как можно заметить, способ с dynamic_cast медленнее двух предложенных способов в несколько раз.
Однако давайте пока не будем торопиться с выводами и посмотрим, как себя поведёт PVS-Studio на реальных проектах.
Реальный бенчмарк
Конечно, хочется ответить на вопрос: а даёт ли это хоть сколько-нибудь производительности анализатору? Очевидно, что большую часть времени анализатор проводит далеко не в функциях преобразования указателей, а, например, в диагностических правилах.
Для замера я поменял наши оптимизированные операции преобразования только в функции, которая производит преобразования между узлами дерева и её специализациях. Стоит отметить, что это не единственная сущность, несущая в себе подобную функциональность.
Замеры мы будем проводить на SelfTester – это наша собственная утилита, которая берёт пул из 123 тестовых проектов и выполняет их анализ при помощи PVS-Studio. Утилита одновременно запускает анализ 4-х проектов, каждый проект проверяется в 8 потоков. В качестве тестовых проектов мы используем реальные проекты с открытым исходным кодом. В результате для каждого проекта генерируется лог предупреждений анализатора. Этот лог сравнивается с эталонным логом для этого же проекта. В ходе сравнения логов SelfTester создаёт журнал сравнения логов в удобном для восприятия разработчиком виде.
Запуск происходил на машине следующей конфигурации:
процессор Intel Core i7-9700k;
32 ГБ RAM;
Samsung SSD 970 EVO Plus 500GB в качестве системного диска;
WDC WD10EZEX-22MFCA0, тут располагается SelfTester.
Для бенчмарка мы будем запускать анализ на проектах дважды. Первый запуск был с ядром, в котором наш аналог dyn_cast был изменён на dynamic_cast. Второй запуск – с нашим оптимизированным способом. Оба запуска производились на последней актуальной версии ядра PVS-Studio.
Результаты замеров на 15 проектах из пула:
Проект | Размер проекта, KLOC | Время: dynamic_cast | Время: проверка + static_cast |
|---|---|---|---|
SObjectizer | 17.2 | 0:01:10 | 0:00:56 |
StrongDC | 102 | 0:00:46 | 0:00:41 |
Notepad++ | 111 | 0:02:48 | 0:02:55 |
WinMerge | 172 | 0:11:02 | 0:09:48 |
db_10 | 213 | 0:36:58 | 0:32:20 |
pcsx2 | 302 | 0:04:44 | 0:04:57 |
dosbox | 302 | 0:05:54 | 0:04:12 |
CamStudio | 327 | 0:09:49 | 0:08:34 |
Shareaza | 400 | 0:40:32 | 0:36:17 |
mpc-hc | 872 | 0:40:46 | 0:34:30 |
QtParts | 1361 | 0:03:31 | 0:02:35 |
miranda32 | 1811 | 0:32:03 | 0:27:07 |
awesome-hpp | 2196 | 0:12:01 | 0:11:37 |
ffdshow | 2213 | 0:36:23 | 0:34:08 |
Freeswitch | 3690 | 0:37:33 | 0:33:30 |
Попробуем немного исключить статистическую погрешность и повторим эксперимент:
Проект | Размер проекта, KLOC | Время: dynamic_cast | Время: проверка + static_cast |
|---|---|---|---|
SObjectizer | 17.2 | 0:01:04 | 0:00:51 |
StrongDC | 102 | 0:00:49 | 0:00:47 |
Notepad++ | 111 | 0:03:11 | 0:02:58 |
WinMerge | 172 | 0:11:23 | 0:09:44 |
db_10 | 213 | 0:37:38 | 0:32:50 |
pcsx2 | 302 | 0:04:49 | 0:04:35 |
dosbox | 302 | 0:06:05 | 0:05:20 |
CamStudio | 327 | 0:10:19 | 0:09:15 |
Shareaza | 400 | 0:40:48 | 0:36:39 |
mpc-hc | 872 | 0:37:37 | 0:34:18 |
QtParts | 1361 | 0:03:52 | 0:03:05 |
miranda32 | 1811 | 0:33:04 | 0:31:28 |
awesome-hpp | 2196 | 0:12:09 | 0:11:18 |
ffdshow | 2213 | 0:39:32 | 0:36:04 |
Freeswitch | 3690 | 0:36:54 | 0:32:16 |

Из бенчмарков заметно, что чем больше размер проекта, тем сильнее влияние dynamic_cast на итоговое время его анализа. А для небольших проектов, которые быстро анализируются, правка не так существенна.
Итоги
В результате действительно можно сказать, что для некоторых проектов отказ от RTTI может дать существенный выигрыш в производительности. Не зря Clang собирается с -fno-rtti. Однако не стоит совсем уж зацикливаться на этом и воевать с RTTI везде, ведь порой удобство написания кода может быть важнее выигрыша в производительности (особенно, если он только гипотетический).
