Pull to refresh

Преобразование обычного класса в странно повторяющийся шаблон

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

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

Итого имеем следующие дополнительные затраты:

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

К счастью, компиляторы сейчас поддерживают такую оптимизацию как девиртуализация (devirtualization). Суть её заключается в том, что виртуальный метод вызывается напрямую, если компилятор точно знает тип вызываемого объекта и таблица виртуальных методов при этом не используется. Такая оптимизация появилась довольно давно. Например, для gcc — начиная с версии 4.7, для clang'a начиная с версии 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 принимает шаблоном порождённый класс и кастует указатель на this к порождённому классу. static_cast производит проверку приведения на уровне компиляции, следовательно, не влияет на производительность. Класс B порождён от класса IA, который в свою очередь шаблонизирован классом B.

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

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

Надеюсь, кому-нибудь заметка будет полезна.
Tags:
Hubs:
Total votes 34: ↑21 and ↓13+8
Comments62

Articles