Диаграмма, показывающая, как шаблоны C++ заменяют несколько перегруженных функций одной обобщённой, уменьшая дублирование код

Шаблоны C++: как избавиться от дублирования кода без потери производительности

ИИ-инструменты 12 июля 2026 г.

Что изменилось в работе над проектом

В команде разработчиков часто встречаются ситуации, когда одна и та же логика применяется к разным типам данных.
Один из коллег заметил, что у нас есть несколько функций process, которые делают почти одно и то же, но для разных структур EventA, EventB, EventC.
Нужно ли писать три одинаковых перегрузки, если можно вынести общую часть в шаблон?
Ответ прост: да, если типы имеют схожие поля, то шаблон позволяет написать код один раз и получить разные версии функции на этапе компиляции.

Источник: Habr

Почему это важно сейчас

В современных C++‑проектах важна не только скорость выполнения, но и читаемость, поддерживаемость и безопасность типов.
Когда код содержит множество перегрузок, он растет, становится труднее отлаживать и тестировать.
Шаблоны дают возможность:

  • Перенести часть решений из времени выполнения в время компиляции – компилятор сам генерирует нужные версии функций, экономя время и уменьшая вероятность ошибок.
  • Определить контракты на уровне типов – через concept можно явно указать, какие поля и типы должны присутствовать у объекта, чтобы он мог быть передан в шаблон.
  • Собрать поведение из политик – шаблоны позволяют комбинировать разные стратегии без лишних виртуальных вызовов.
  • Писать обобщённый код без лишней runtime‑стоимости – шаблоны компилируются в конкретный код, а не в динамическую таблицу виртуальных функций.

Когда стоит применять шаблоны

  1. Общие поля у разных структур
    Если у нескольких структур есть одинаковые поля (например, count и value), можно вынести логику работы с этими полями в шаблон.
    cpp template <typename Event> void process(const Event& event) { std::cout << event.count << '\n'; std::cout << event.value << '\n'; } Компилятор создаст отдельную версию функции для каждого конкретного типа.
  2. Нужен compile‑time контракт
    Если вы хотите гарантировать, что переданный объект имеет определённые поля, используйте concept.
    cpp template <typename T> concept BasicEvent = requires(T& e) { { e.count } -> std::convertible_to<std::uint64_t>; { e.value } -> std::convertible_to<double>; }; template <BasicEvent Event> void process(const Event& event) { std::cout << "count = " << event.count << '\n'; std::cout << "value = " << event.value << '\n'; } При попытке передать объект, не удовлетворяющий контракту, компилятор выдаст ошибку, а не тайный баг в рантайме.
  3. Нужно скрыть реализацию
    Если вы хотите предоставить клиенту единый интерфейс, но скрыть детали реализации, применяйте тип‑эрейшн.
    cpp class IEventProcessor { public: virtual void process(const EventBase& e) = 0; }; При этом можно использовать std::unique_ptr<IEventProcessor> вместо виртуальных функций, если хотите избежать таблицы виртуальных функций.

Как использовать compile‑time dispatch, type traits и type erasure

Техника Что делает Когда применить
Compile‑time dispatch Компилятор генерирует конкретную реализацию функции для каждого типа. Когда логика одинаковая, но типы различаются.
Type traits Позволяют получить свойства типа в compile‑time (например, наличие поля count). Когда нужно проверить свойства типа без реального объекта.
Concepts Явно задают контракт, который должен удовлетворять тип. Когда хотите, чтобы компилятор проверил наличие нужных полей/методов.
Type erasure Скрывает конкретный тип за общим интерфейсом, избавляя от виртуальных вызовов. Когда нужна гибкость интерфейса, но без накладных расходов виртуальных таблиц.

Пример: упрощение кода с помощью шаблонов

struct EventA { std::uint64_t count = 0; double value = 0.0; };
struct EventB { std::uint64_t count = 0; double value = 0.0; double rate = 0.0; };
struct EventC { std::uint64_t count = 0; double value = 0.0; std::string label; };

template <typename Event>
void process(const Event& event) {
    std::cout << event.count << '\n';
    std::cout << event.value << '\n';
}

Теперь вместо трёх перегрузок у нас одна функция, а компилятор сам создаст нужные версии. Это упрощает поддержку и уменьшает вероятность ошибок.

Пример: использование концепта

template <typename T>
concept BasicEvent = requires(T& e) {
    { e.count } -> std::convertible_to<std::uint64_t>;
    { e.value } -> std::convertible_to<double>;
};

template <BasicEvent Event>
void process(const Event& event) {
    std::cout << "count = " << event.count << '\n';
    std::cout << "value = " << event.value << '\n';
}

Если попытаться вызвать process с объектом, у которого нет поля count, компилятор выдаст ошибку, а не тайный баг в рантайме.

Что может пойти не так

  • Неправильное применение шаблонов – если типы сильно различаются, вынесение общей логики может привести к неочевидным ошибкам.
  • Сложность чтения кода – шаблоны иногда делают код менее читаемым для тех, кто не знаком с метапрограммированием.
  • Проблемы с отладкой – ошибки в шаблонах часто проявляются в длинных сообщениях компилятора, что затрудняет поиск причины.
  • Неверные предположения о типах – если вы используете concept, но не проверяете, что тип действительно удовлетворяет контракту, компилятор может сгенерировать ошибку в другом месте, чем ожидалось.

Что проверить и как действовать дальше

  1. Идентифицируйте повторяющуюся логику – просмотрите код на наличие похожих функций/методов, которые работают с разными типами.
  2. Проверьте наличие общих полей – если типы имеют одинаковые поля, шаблон может быть полезен.
  3. Определите, нужен ли вам compile‑time контракт – если важно гарантировать наличие определённых членов, используйте concept.
  4. Оцените сложность кода – если шаблоны делают код слишком сложным для команды, возможно, стоит оставить обычные функции.
  5. Проверьте производительность – сравните время компиляции и размер бинарного файла с и без шаблонов.
  6. Проведите тестирование – убедитесь, что все типы корректно обрабатываются и ошибки компиляции появляются только при нарушении контракта.

Практический чек‑лист

Шаг Что проверить Как проверить
1 Есть ли несколько перегрузок одной функции? Просмотрите заголовки и реализации.
2 Совпадают ли поля у разных структур? Сравните списки полей в структурах.
3 Можно ли вынести общую логику в шаблон? Попробуйте написать шаблонную версию и убедитесь, что она компилируется.
4 Требуется ли compile‑time контракт? Определите, какие свойства должны быть у типа, и создайте concept.
5 Есть ли риск потери читаемости? Оцените, насколько легко понять шаблонный код для команды.
6 Какой эффект на производительность? Сравните время компиляции и размер бинарного файла.
7 Как тестировать? Напишите unit‑тесты для каждой конкретной версии шаблона.

Источники

Что почитать дальше

Теги