﻿# Есть ли жизнь без RTTI: пишем свой dynamic\_cast

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

![0998_Implement_dynamic_cast_ru/image1.png](https://import.viva64.com/docx/blog/0998_Implement_dynamic_cast_ru/image1.png)

## Об исследуемой области

Давайте вспомним немного азов языка C\+\+\. Если вам покажется скучной эта часть, вы всегда можете перейти к следующей главе\.

Итак, представим, что у нас есть следующая иерархия классов:

```cpp
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_\) из указателя на объект дочернего класса к указателю на объект базового класса:

```cpp
#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_](https://en.cppreference.com/w/cpp/language/dynamic_cast) и [_typeid_](https://en.cppreference.com/w/cpp/language/typeid)\. Стандарт C\+\+ не определяет, как именно реализуется RTTI, и вся ответственность перекладывается на _двоичный интерфейс приложений_ \(_application binary interface_, сокр\. _ABI_\)\. В большинстве компиляторов информация хранится в виртуальной таблице \(_vtable_\)\. Если вам интересны детали, то в качестве одного из примеров можно ознакомиться с имплементацией на платформе [x86\_64](https://itanium-cxx-abi.github.io/cxx-abi/abi.html#rtti)\.

Чтобы корректно произвести "сужающие" преобразования \(_downcasting_\) из указателя на объект базового класса к указателю на объект дочернего класса, воспользуемся оператором _dynamic\_cast_:

```cpp
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_](https://clang.llvm.org/doxygen/classclang_1_1Decl.html) \(интерфейс для объявлений функций и типов\) и [_Stmt_](https://clang.llvm.org/doxygen/classclang_1_1Stmt.html) \(интерфейс для конструкций\)\. 

Рассмотрим пример\. Допустим, у нас есть следующий синтетический блок кода:

```cpp
{
  for (size_t i = 0; i < 10; ++i) ;
  if (true) ;
}
```

Согласно грамматикам языков [C](https://en.cppreference.com/w/c/language/statements) и [C\+\+](https://en.cppreference.com/w/cpp/language/statements), это составная конструкция \(блок\), содержащая цикл _for_ и ветвление _if_\. Если попробовать реализовать это в объектно\-ориентированной парадигме, у нас будут следующие сущности:

```cpp
// 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_\. Теперь мы можем перебрать все конструкции:

```cpp
void foo(const CompoundStmt *compStmt)
{
  for (auto stmt : compStmt)
  {
    // do smth
  }
}
```

И последнее, что мы хотим сделать, это позвать для каждого специализированного узла свою кастомную логику:

```cpp
void Visit(const IfStmt &);
void Visit(const ForStmt &);
```

И здесь мы как раз сталкиваемся с проблемой – необходимо узнать на этапе работы программы, какая именно конструкция лежит под указателем на _Stmt_, и позвать нужную перегрузку:

```cpp
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_ работать не будет\. Чтобы исправить проблему, нужно добавить хотя бы одну виртуальную функцию\. Например, так:

```cpp
struct Stmt { virtual void Dummy(); /* .... */ };
```

Теперь всё хорошо и код будет работать в большинстве случаев\. Пока не окажется, что в проекте по разным объективным причинам выключен RTTI\. Например, в компиляторах GCC/Clang это можно сделать, передав флаг _\-fno\-rtti_ \(GCC, Clang\), в MSVC – посредством _/GR\-_\. Стоит отметить, что выключение RTTI не задевает [механизм исключений](https://godbolt.org/z/zcso7rTcz) и [динамическую диспетчеризацию](https://godbolt.org/z/KeEM88vP9)\.

Кстати, пока готовил статью, нашёл интересный опрос на [isocpp\.org](https://isocpp.org/files/papers/CppDevSurvey-2019-04-summary.pdf)\. В нём из _2058_ респондентов _14%_ ответили, что они частично отключают RTTI на своих программах, а _18%_ отключают его полностью\.

Подытожим, _dynamic\_cast_ плох тем, что:

* Работает только на полиморфных типах\.
* Не работает с флагом _\-fno\-rtti_\.
* Для работы требуется ABI\-специфичная информация о типах\.
* "Сужающие" преобразования работают через просмотр _vtable,_ а это медленная операция\.

Одним из решений проблемы может быть использование паттерна проектирования "[посетитель](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D1%81%D0%B5%D1%82%D0%B8%D1%82%D0%B5%D0%BB%D1%8C_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F))" \(визитор\)\. Оно избавит от необходимости производить _dynamic\_cast_ ценой одного\-двух вызовов виртуальной функции\. Однако давайте посмотрим, как можно поступить иначе\.

## Пишем свой dynamic\_cast

### Это база

Рецепт лечения на самом деле прост\. Мы внедрим в родительский класс поле с информацией о типе и будем сами проверять её\. Это может выглядеть так:

```cpp
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_\. Теперь мы можем проверять тип следующим образом:

```cpp
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_ не умеет работать с [виртуальным наследованием](https://godbolt.org/z/Kb175eh4E) и код не скомпилируется\. Но много ли людей использует виртуальное наследование в 2022 году?

Посмотрев на код, можно задаться вопросом: "А где здесь аналог _dynamic\_cast_"? Конечно, писать проверки именно таким образом не очень удобно\. Чтоб исправить это, добавим немного абстракций\. Рассмотрим, как это реализовано у нас в PVS\-Studio и в LLVM\.

### Наводим красоту\. Подход в PVS\-Studio

Вернёмся к нашему примеру:

```cpp
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_\.

```cpp
template <typename T, typename Kind>
bool IsA(T *p, Kind kind) noexcept
{
  return p && p->Kind() == kind;
}
```

Далее реализуем пару шаблонов структур без определения:

```cpp
template <Stmt::Type K>
struct type_from_kind;

template <typename T>
struct kind_from_type;
```

Они нужны для того, чтобы потом специализировать их для каждого производного класса\. Таким образом, мы свяжем тип узла дерева и константу перечисления, находящуюся внутри класса\.

```cpp
template <> struct type_from_kind<Stmt::IfStmt>
{
  using type = IfStmt;
};

template <> struct kind_from_type<IfStmt>
{
  static constexpr auto value = Stmt::Type::IfStmt;
};
```

Конечно, писать такие специализации для каждого класса не очень приятно\. Тут нам на помощь может прийти "макросная магия":

```cpp
#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; \
  };
```

Тогда определение дочерних классов будет выглядеть так:

```cpp
struct IfStmt : Stmt { /* .... */ };
MAKE_STMT_TRAITS(IfStmt, Stmt::IfStmt)

struct ForStmt : Stmt { /* .... */ };
MAKE_STMT_TRAITS(ForStmt, Stmt::ForStmt)
```

Теперь можно реализовать свой _dyn\_cast_, инкапсулирующий _static\_cast_ c проверкой на нужный тип через функцию _IsA_:

```cpp
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_:

```cpp
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_:

```cpp
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 дочернего класса:

```cpp
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_ из типа, который нам передали первым шаблонным параметром:

```cpp
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_ к нужному типу или возвращать нулевой указатель:

```cpp
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_\.

### Синтетический пример

Синтетический бенчмарк выглядит следующим образом \([ссылка](https://quick-bench.com/q/v-3hraub6v8nlitjtx8ybgDT7PA) на Quick C\+\+ Benchmark\):

1. Определяем структуру _Stmt\_RTTI_, от которой будут наследоваться структуры _IfStmt\_RTTI_ и _ForStmt\_RTTI_\. Их мы впоследствии будем преобразовывать при помощи _dynamic\_cast\._
1. Определяем структуру _Stmt\_WithEnum_, от которого будут наследоваться _IfStmt\_WithEnum_ и _ForStmt\_WithEnum_\. Их мы будем преобразовывать при помощи проверки функцией _IsA_, реализованной двумя вышеописанными способами, и преобразованием при помощи _static\_cast_\.
1. Формируем вектор из 1'000'000 умных указателей на тип _Stmt\_RTTI_ / _Stmt\_WithEnum_, инициализируем их псевдослучайно наследниками\.
1. Итерируемся по вектору и делаем преобразование к одному из дочерних типов\.

<details>
   <summary>Код бенчмарка</summary>

```cpp
#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);
```


</details>
Результаты:

![0998_Implement_dynamic_cast_ru/image2.png](https://import.viva64.com/docx/blog/0998_Implement_dynamic_cast_ru/image2.png)

Как можно заметить, способ с _dynamic\_cast_ медленнее двух предложенных способов в 3\-4 раза\. 

Однако давайте пока не будем торопиться с выводами и посмотрим, как себя поведёт 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|

![0998_Implement_dynamic_cast_ru/image4.png](https://import.viva64.com/docx/blog/0998_Implement_dynamic_cast_ru/image4.png)

Из бенчмарков заметно, что чем больше размер проекта, тем сильнее влияние _dynamic\_cast_ на итоговое время его анализа\. А для небольших проектов, которые быстро анализируются, правка не так существенна\.

## Итоги

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