[Из песочницы] Преобразование обычного класса в странно повторяющийся шаблон
Давайте вспомним, как работают виртуальные методы. Если класс содержит один или более виртуальный метод, компилятор для такого класса создает таблицу виртуальных методов, а в сам класс добавляет виртуальный табличный указатель. Компилятор также генерирует код в конструкторе класса для инициализации виртуального табличного указателя. Выбор вызываемого виртуального метода производится на этапе выполнения программы при помощи выбора адреса метода из созданной таблицы.
Итого имеем следующие дополнительные затраты:
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:
helloFunction(){
std::cout<< "Hello from B";
}
};
Превращается в:
template
class IA {
public:
helloFunction(){
static_cast(this)->helloFunction();
}
};
class B : public IA {
public:
helloFunction(){
std::cout<< "Hello from B";
}
};
Обращение:
template
void sayHello(IA* object) {
object->helloFunction();
}
Класс IA принимает шаблоном порождённый класс и кастует указатель на this к порождённому классу. static_cast производит проверку приведения на уровне компиляции, следовательно, не влияет на производительность. Класс B порождён от класса IA, который в свою очередь шаблонизирован классом B.
Дополнительные затраты — дополнительный указатель в классе, дополнительный код в конструкторе класса, дополнительный код при каждом вызове виртуального метода, как в первом случае отсутствуют. Если ваш компилятор не поддерживает оптимизацию девиртуализации, то такой код будет работать быстрее и занимать меньше памяти.
Спасибо за внимание.
Надеюсь, кому-нибудь заметка будет полезна.
Комментарии (1)
16 августа 2016 в 20:34
+2↑
↓
Так и не понял почему же вызов виртуальных методов «тормозил» у новоиспеченных программистов.