Перейти к содержанию

Странно рекурсивный шаблонный паттерн (CRTP) и статический полиморфизм

Начало урока

Странно рекурсивный шаблонный паттерн (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 позволяет сохранить форму «вызвать реализацию производного класса», разрешая её на этапе компиляции.

Продолжите этот урок на YourAIZone

В полном уроке — объяснение целиком, разобранные примеры, частые ошибки, тесты с мгновенной проверкой и ИИ-репетитор. Начните сразу как гость или создайте аккаунт, чтобы сохранять прогресс.

В полном уроке

  • Объяснение целиком
  • Разобранные примеры
  • Частые ошибки и как их избежать
  • Тестовые вопросы: 15
  • ИИ-репетитор, который отвечает на вопросы по теме
  • Отслеживание прогресса для ученика, родителей и учителя