Странно рекурсивный шаблонный паттерн (CRTP) и статический полиморфизм
Диспетчеризация на этапе компиляции как альтернатива виртуальным функциям без накладных расходов
class Widget : public Base<Widget>. Поскольку Base знает точный производный тип уже на этапе компиляции, он может напрямую вызывать методы производного класса через static_cast, без таблицы виртуальных функций, без косвенного вызова и с полным встраиванием. Так полиморфизм превращается в решение, принимаемое на этапе компиляции.
- объяснить стоимость виртуальной диспетчеризации во время выполнения и когда её стоит избегать,
- читать и писать конструкцию CRTP и понимать, почему
static_castздесь безопасен, - использовать CRTP для статических интерфейсов, внедрения общего поведения как примесей и генерации шаблонного кода операторов,
- честно взвешивать диспетчеризацию на этапе компиляции против диспетчеризации во время выполнения и выбирать подходящий инструмент.
1. Повторение: во что обходится полиморфизм во время выполнения
Вы уже знаете виртуальные функции из начального и среднего курсов. Когда вы помечаете метод как virtual и вызываете его через указатель или ссылку на базовый класс, компилятор не может знать конкретный тип, поэтому диспетчеризация происходит во время выполнения через таблицу виртуальных функций (vtable): каждый полиморфный объект несёт скрытый vptr, указывающий на таблицу адресов функций своего класса.
Эта гибкость не бесплатна. Каждый виртуальный вызов - это косвенный вызов через указатель, который компилятор обычно не может предсказать, а значит:
- цель неизвестна на этапе компиляции, поэтому вызов нельзя встроить, и оптимизатор не видит сквозь него;
- каждый объект увеличивается на один указатель (
vptr), что важно для мелких и многочисленных объектов; - косвенность может стоить ошибки предсказания перехода или промаха кеша в горячих циклах.
struct Shape {
virtual double area() const = 0; // разрешается во время выполнения через vtable
virtual ~Shape() = default;
};
double totalArea(const std::vector<Shape*>& v) {
double s = 0;
for (const Shape* p : v)
s += p->area(); // косвенный вызов, встроить нельзя
return s;
}
Для большинства кода эта стоимость незначительна, а гибкость - именно то, что нужно. Но когда конкретный тип уже известен на этапе компиляции - численное ядро, шаблон выражения, адаптер контейнера - платить за динамическую диспетчеризацию значит тратить впустую. CRTP позволяет сохранить форму «вызвать реализацию производного класса», разрешая её на этапе компиляции.