Превращение Обычного Класса В Странно Повторяющийся Узор

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

Я был очень удивлен.

Давайте вспомним, как работают виртуальные методы.

Если класс содержит один или несколько виртуальных методов, компилятор создает таблицу виртуальных методов для этого класса и добавляет указатель виртуальной таблицы на сам класс.

Компилятор также генерирует код в конструкторе класса для инициализации указателя виртуальной таблицы.

Выбор вызываемого виртуального метода осуществляется на этапе выполнения программы путем выбора адреса метода из созданной таблицы.

Итого имеем следующие дополнительные расходы: 1) Дополнительный указатель в классе (указатель на таблицу виртуальных методов); 2) Дополнительный код в конструкторе класса (для инициализации указателя виртуальной таблицы); 3) Дополнительный код для каждого вызова виртуального метода (разыменование указателя на таблицу виртуальных методов и поиск в таблице нужного адреса виртуального метода).

К счастью, компиляторы теперь поддерживают такие оптимизации, как девиртуализация.

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

Эта оптимизация существует уже довольно давно.

Например, для gcc — начиная с версии 4.7, для clang начиная с версии 3.8 (появился флаг -fstrict-vtable-pointers).

Но все же, можно ли вообще использовать полиморфизм без виртуальных функций? Ответ: да, можете.

На помощь приходит так называемый Curiously Recurring Template Pattern (CRTP).

Правда, это уже будет статический полиморфизм.

Он отличается от обычного динамического.

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

  
  
   

class IA { public: virtual void helloFunction() = 0; }; class B : public IA { public: void helloFunction(){ std::cout<< "Hello from B"; } };

Превращается в:

template <typename T> class IA { public: void helloFunction(){ static_cast<T*>(this)->helloFunction(); } }; class B : public IA<B> { public: void helloFunction(){ std::cout<< "Hello from B"; } };

Обращаться:

template <typename T> void sayHello(IA<T>* object) { object->helloFunction(); }

Класс IA принимает сгенерированный класс в качестве шаблона и передает указатель на него в сгенерированный класс.

static_cast выполняет проверку приведения на уровне компиляции и поэтому не влияет на производительность.

Класс B является производным от класса IA, который, в свою очередь, создан по шаблону класса B. Дополнительные затраты — дополнительный указатель в классе, дополнительный код в конструкторе класса, дополнительный код для каждого вызова виртуального метода, как и в первом случае, отсутствуют. Если ваш компилятор не поддерживает оптимизацию девиртуализации, то такой код будет работать быстрее и занимать меньше памяти.

Спасибо за внимание.

Надеюсь, эта заметка будет кому-то полезна.

Теги: #c plus plus #программирование #шаблоны C++ #виртуальные функции #C++

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.