Перейти к содержимому

Шаблоны шаблонов метапрограммирования

Мастер расширенных шаблонов шаблонов: CRTP для статического полиморфизма, дизайн на основе политики для гибкой композиции, стирание типов для дженериков времени выполнения и руководящие принципы для использования шаблонов против виртуальной отправки.

За пределами основных шаблонов

Шаблоны позволяют создавать шаблоны, которые не имеют эквивалента на других языках. Эти шаблоны используют компиляционную природу шаблонов для достижения статического полиморфизма (без накладных расходов на виртуальные функции), встраиваемого поведения (политики) и безопасного стирания (скрытие конкретных типов за единым интерфейсом).

Это не шаблоны для начинающих — это инструменты для авторов библиотек и дизайнеров фреймворков. Понимание их важно для чтения стандартных внутренних библиотек, Boost и современных фреймворков C++.

CRTP: любопытно повторяющийся шаблон

В CRTP класс происходит от шаблона, инстанцированного с itself в качестве аргумента: class Derived : public Base. Базовый класс может вызывать методы по производному классу при времени компиляции — без виртуальной отправки накладных расходов. Это используется для инъекционного поведения, реализации статического полиморфизма и создания функциональности миксина.

crtp.cpp
#include <iostream>

// CRTP base: provides a counter for each derived class
template <typename Derived>
class Counter {
    static inline int count_ = 0;

protected:
    Counter()  { ++count_; }
    ~Counter() { --count_; }
    Counter(const Counter&) { ++count_; }

public:
    static int alive() { return count_; }
};

// Each derived class gets its OWN counter
class Dog : public Counter<Dog> {
public:
    std::string name;
    Dog(std::string n) : name(std::move(n)) {}
};

class Cat : public Counter<Cat> {
public:
    std::string name;
    Cat(std::string n) : name(std::move(n)) {}
};

// CRTP for static polymorphism: no virtual dispatch
template <typename Derived>
class Shape {
public:
    double area() const {
        // Call derived class method — resolved at compile time
        return static_cast<const Derived*>(this)->area_impl();
    }
};

class Circle : public Shape<Circle> {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double area_impl() const { return 3.14159265 * radius_ * radius_; }
};

class Square : public Shape<Square> {
    double side_;
public:
    Square(double s) : side_(s) {}
    double area_impl() const { return side_ * side_; }
};

int main() {
    Dog d1("Rex"), d2("Buddy");
    Cat c1("Whiskers");
    std::cout << "Dogs alive: " << Dog::alive() << '\n'; // 2
    std::cout << "Cats alive: " << Cat::alive() << '\n'; // 1

    Circle circle(5.0);
    Square square(4.0);
    std::cout << "Circle area: " << circle.area() << '\n'; // 78.54
    std::cout << "Square area: " << square.area() << '\n'; // 16.0
}

Дизайн на основе политики

Основанный на политике дизайн разлагает класс на взаимозаменяемые поведенческие компоненты (политики), передаваемые в качестве шаблонных параметров. Каждая политика реализует один аспект поведения. Пользователи составляют именно ту комбинацию, которая им нужна. Это было популяризировано Андреем Александреску «Современный C++». Дизайн.

policy_design.cpp
#include <iostream>
#include <mutex>
#include <stdexcept>

// Threading policies
struct SingleThreaded {
    struct Lock { Lock(SingleThreaded&) {} }; // no-op
};

struct MultiThreaded {
    std::mutex mtx;
    struct Lock {
        std::lock_guard<std::mutex> guard;
        Lock(MultiThreaded& host) : guard(host.mtx) {}
    };
};

// Bounds checking policies
struct NoBoundsCheck {
    static void check(int /*index*/, int /*size*/) {} // no-op
};

struct StrictBoundsCheck {
    static void check(int index, int size) {
        if (index < 0 || index >= size)
            throw std::out_of_range("Index out of bounds");
    }
};

// Policy-based container: users choose their combination
template <typename T,
         typename ThreadingPolicy = SingleThreaded,
         typename BoundsPolicy = NoBoundsCheck>
class SmartArray : private ThreadingPolicy {
    T* data_;
    int size_;

public:
    SmartArray(int size) : data_(new T[size]{}), size_(size) {}
    ~SmartArray() { delete[] data_; }

    T& operator[](int index) {
        typename ThreadingPolicy::Lock lock(*this);
        BoundsPolicy::check(index, size_);
        return data_[index];
    }

    int size() const { return size_; }
};

int main() {
    // Fast, no overhead: no locking, no bounds checks
    SmartArray<int> fast(10);
    fast[0] = 42;

    // Safe: bounds-checked, single-threaded
    SmartArray<int, SingleThreaded, StrictBoundsCheck> safe(10);
    safe[0] = 42;
    try {
        safe[99] = 0; // throws!
    } catch (const std::out_of_range& e) {
        std::cout << e.what() << '\n';
    }

    // Thread-safe with bounds checking
    SmartArray<int, MultiThreaded, StrictBoundsCheck> robust(10);
    robust[5] = 100;
}

Стирание типа

Стирание типа скрывает конкретный тип за однородным интерфейсом, сочетая гибкость шаблонов с полиморфизмом времени выполнения виртуальных функций. Классическим примером является std::function, который может содержать любой вызывающий. В шаблоне используются три компонента: концепция (абстрактный интерфейс), модель (обертка шаблона, реализующая интерфейс для любого типа) и класс внешняя ручка.

type_erasure.cpp
#include <iostream>
#include <memory>
#include <string>

// Type-erased "Printable" — can hold any type with a print() method
class Printable {
    // Concept: abstract interface
    struct Concept {
        virtual ~Concept() = default;
        virtual void print(std::ostream& os) const = 0;
        virtual std::unique_ptr<Concept> clone() const = 0;
    };

    // Model: wraps any T that supports operator<<
    template <typename T>
    struct Model : Concept {
        T value;
        Model(T v) : value(std::move(v)) {}
        void print(std::ostream& os) const override { os << value; }
        std::unique_ptr<Concept> clone() const override {
            return std::make_unique<Model>(value);
        }
    };

    std::unique_ptr<Concept> ptr_;

public:
    // Constructor: accepts any printable type
    template <typename T>
    Printable(T value) : ptr_(std::make_unique<Model<T>>(std::move(value))) {}

    // Copy support
    Printable(const Printable& other) : ptr_(other.ptr_->clone()) {}
    Printable& operator=(const Printable& other) {
        ptr_ = other.ptr_->clone();
        return *this;
    }
    Printable(Printable&&) = default;
    Printable& operator=(Printable&&) = default;

    friend std::ostream& operator<<(std::ostream& os, const Printable& p) {
        p.ptr_->print(os);
        return os;
    }
};

int main() {
    // Heterogeneous collection — no common base class needed!
    std::vector<Printable> items;
    items.emplace_back(42);
    items.emplace_back(3.14);
    items.emplace_back(std::string("hello"));
    items.emplace_back('X');

    for (const auto& item : items) {
        std::cout << item << '\n';
    }
    // Output: 42, 3.14, hello, X
}

Шаблоны выражения (обзор)

Шаблоны выражений — это метод, в котором арифметические выражения на объектах строят дерево операций, а не вычисляют промежуточные результаты. Окончательный результат вычисляется в один проход, устраняя временные объекты.

Например, с наивным классом Vector a + b + c создает два временных вектора. С помощью шаблонов экспрессии a + b + c создает легкий объект экспрессии Add, Vector>, который вычисляет элементы по запросу.

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

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

Лучшая практика

При выборе между шаблонами (статический полиморфизм) и виртуальными функциями (динамический полиморфизм):

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

Используйте виртуальные функции, когда:
Набор типов открыт и определяется во время выполнения (плагины, пользовательский ввод)
- Неоднородные контейнеры (например, vector>)
Бинарный размер важнее скорости (шаблоны генерируют больше кода)
- Вы хотите стабильный ABI через границы общей библиотеки
Добавление новых типов без перекомпилирования существующего кода

Используйте стирание типа, когда:
Вы хотите простоту интерфейса виртуальной отправки, но не хотите заставлять пользователей наследовать от вашего базового класса.
Семантика значений с полиморфизмом во время выполнения (std::function, std::any)

Подводный камень

Метапрограммирование шаблонов является мощным, но имеет реальные затраты:

1. Взрыв времени компиляции — глубоко вложенные шаблонные инстанциации могут сделать время компиляции неприемлемым. Рекурсивный TMP особенно плох — предпочитают сгибающиеся выражения и if constexpr.
2. Ошибочные сообщения — без понятий шаблонные ошибки создают страницы непонятного текста. Всегда ограничивайте шаблоны.
3. Отладка — прохождение через тяжелый код шаблона в отладчике болезненно. Типовые обертки добавляют опосредованность, которая скрывает фактический тип.
4. Readability — CRTP, основанный на политике дизайн и шаблоны выражения трудно понять младшим разработчикам. Документируйте название шаблона и намерение.
5.Инженерия — не каждая проблема нуждается в шаблонном решении. Простая виртуальная функция или даже заявление переключателя могут быть более четкими и достаточными.

Ключевые выводы
  • CRTP обеспечивает статический полиморфизм и счетчики / миксины для каждого класса, полученные из Base
  • Дизайн, основанный на политике, разлагает поведение на параметры шаблона — пользователи составляют точную комбинацию, которая им нужна.
  • Стирание типов сочетает гибкость шаблона с полиморфизмом среды выполнения (например, std::function, std::any)
  • Шаблоны экспрессии устраняют временные интервалы в цепных операциях, используемых Eigen, Blaze и подобными библиотеками.
  • Выберите шаблоны для известных типов и нулевых накладных расходов; виртуальная отправка для наборов открытых типов и гибкость во время выполнения
  • Избегайте чрезмерной инженерии — используйте самый простой механизм, который соответствует вашим требованиям.

Тест — проверьте свои знания

(20 XP)

1. Что такое `class Dog : public Counter<Dog>`?

2. Какую проблему решает стирание типов?

3. Когда вы предпочитаете виртуальные функции шаблонам?