惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Forbes - Security
Forbes - Security
The Register - Security
The Register - Security
G
Google Developers Blog
罗磊的独立博客
WordPress大学
WordPress大学
L
LangChain Blog
博客园 - 三生石上(FineUI控件)
Recorded Future
Recorded Future
Microsoft Azure Blog
Microsoft Azure Blog
Google DeepMind News
Google DeepMind News
大猫的无限游戏
大猫的无限游戏
MongoDB | Blog
MongoDB | Blog
小众软件
小众软件
Recent Announcements
Recent Announcements
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
云风的 BLOG
云风的 BLOG
Apple Machine Learning Research
Apple Machine Learning Research
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
F
Full Disclosure
The Cloudflare Blog
B
Blog RSS Feed
S
Schneier on Security
T
Tenable Blog
人人都是产品经理
人人都是产品经理
Engineering at Meta
Engineering at Meta
T
Tor Project blog
N
Netflix TechBlog - Medium
T
Threatpost
NISL@THU
NISL@THU
Stack Overflow Blog
Stack Overflow Blog
N
News | PayPal Newsroom
N
News and Events Feed by Topic
aimingoo的专栏
aimingoo的专栏
博客园 - 叶小钗
P
Privacy & Cybersecurity Law Blog
C
CERT Recently Published Vulnerability Notes
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy International News Feed
博客园 - 【当耐特】
Help Net Security
Help Net Security
The Hacker News
The Hacker News
Hugging Face - Blog
Hugging Face - Blog
TaoSecurity Blog
TaoSecurity Blog
S
Secure Thoughts
C
Cisco Blogs
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Шаблоны C++ как инструмент архитектуры: compile-time dispatch, type traits и type erasure
Матвей Жуков · 2026-06-28 · via Все публикации подряд на Хабре

Всех приветствую! Я неоднократно встречал разработчиков, которые говорили, что метапрограммирование — это моветон, а шаблоны только усложняют код. Я понимаю, откуда берётся такое мнение потому, что при неаккуратном использовании шаблоны действительно могут сделать код сложным и тяжёлым для чтения.

Но, на мой взгляд, проблема не в самом инструменте, а в том, как именно его применяют.

Шаблоны в C++ - это не только std::vector и универсальные функции. В серьёзном C++ они часто используются как архитектурный механизм, позволяют переносить часть решений из runtime в compile-time, задавать контракты на уровне типов, собирать поведение из политик и писать обобщённый код без лишней runtime-стоимости.

Где метапрограммирование действительно нужно

Главное, что хочу сказать:

Метапрограммирование не нужно ради самого метапрограммирования.

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

Но есть случаи, где compile-time подход действительно оправдан.

Если разные типы данных имеют разные возможности, лучше выразить это на уровне типов, а не держать всё в одной универсальной структуре.

Например:

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;
};

У всех событий есть count и value, но дополнительные поля отличаются. Если положить всё в одну структуру с enum, корректность будет держаться на соглашениях. В добавок если разделить типы, компилятор сам начнёт защищать от некорректных обращений.

Когда нужна общая логика для разных типов

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

Для каждого конкретного Event, с которым будет вызвана эта функция, компилятор создаст свою версию. Он видит реальный тип и может оптимизировать код как работу с обычной конкретной структурой. На практике такой подход часто читается проще, чем набор почти одинаковых перегрузок:

void process(const EventA& event);
void process(const EventB& event);
void process(const EventC& event);

Если логика у типов общая, шаблон позволяет описать её один раз и не дублировать код.

Когда нужны compile-time контракты

В функции выше template void process(const Event& event) имеются скрытые требования к типу. Она ожидает, что у объекта есть поля count и value.

В С++20 нам завезли concept и благодаря им эти требования можно выразить явно:

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

Немного подправим код и получим в результате:

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

И тут мы получаем уже не просто шаблонность, а контракт на уровне компиляции!

Например, структура EventA этому контракту соответствует, у нее есть поле count, которое приводится к std::uint64_t и поле value, которое приводится к double.

Теперь специально изменим контракт так, чтобы он больше не подходил для EventA:

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

Теперь мы требуем, чтобы event.count можно было привести к std::string. Но в EventA поле count имеет тип std::uint64_t, поэтому тип больше не удовлетворяет BasicEvent.

При попытке вызвать функцию:

EventA a;
process(a);

компилятор выдаст ошибку:

./m.cpp: In function ‘int main()’:
./m.cpp:40:17: error: no matching function for call to ‘process(EventA&)’
   40 |     process(a);
      |     ~~~~~~~~~~~~^~~
./m.cpp:31:34: note: candidate: ‘template<class Event>  requires  BasicEvent<Event> void process(const Event&)’
   31 | template <BasicEvent Event> void process(const Event &event)
      |                                  ^~~~~~~~~~~~
./m.cpp:31:34: note:   template argument deduction/substitution failed:
./m.cpp:31:34: note: constraints not satisfied
./m.cpp: In substitution of ‘template<class Event>  requires  BasicEvent<Event> void process(const Event&) [with Event = EventA]’:
./m.cpp:40:17:   required from here
./m.cpp:26:9:   required for the satisfaction of ‘BasicEvent<Event>’ [with Event = EventA]
./m.cpp:26:22:   in requirements with ‘T event’ [with T = EventA]
./m.cpp:27:13: note: ‘event.count’ does not satisfy return-type-requirement
   27 |     { event.count } -> std::convertible_to<std::string>;
      |       ~~~~~~^~~~~
cc1plus: note: set ‘-fconcepts-diagnostics-depth=’ to at least 2 for more detail

И это как раз тот момент, ради которого полезны concept, компилятор не просто говорит, что где-то внутри шаблонной функции что-то сломалось. Он показывает, что тип EventA не прошёл проверку контракта BasicEvent.

В данном случае проблема конкретно здесь:

{ event.count } -> std::convertible_to<std::string>;

То есть event.count существует, но его тип не соответствует требованию. Мы попросили std::string, а получили std::uint64_t

Такой подход делает шаблонный код понятнее и требования к типу находятся рядом с объявлением функции, а ошибка возникает на этапе компиляции и указывает именно на нарушенный контракт.

Когда поведение можно собрать на этапе компиляции

Если класс содержит много if, switch и enum-конфигураций, часто это сигнал, что часть поведения можно вынести в policy-типы.

template <typename FilterPolicy, typename ScorePolicy, typename ExportPolicy>
class Pipeline {
public:
    void process(double value) {
        if (!filter_.accept(value)) {
            return;
        }

        auto result = score_.calculate(value);

        if (result.active) {
            export_.send(result);
        }
    }

private:
    FilterPolicy filter_;
    ScorePolicy score_;
    ExportPolicy export_;
};

Конкретное поведение собирается на этапе компиляции:

using DebugPipeline = Pipeline<ThresholdFilter,SimpleScore,ConsoleExport>;

Выигрыш в том, что внутри нет виртуальных вызовов и runtime-ветвления по типу поведения. Компилятор видит весь pipeline целиком.

Compile-time dispatch через if constexpr

Теперь представим, что возникла ситуацию когда у всех структур-событий есть общие поля count и value, но у некоторых есть ещё дополнительные данные, а мы хотим написать одну функцию обработки.

Например, у EventB есть rate, а у EventC есть label.

Вот тут то к нам и приходит на помощь if constexpr вместе с std::is_same_v<>

template <typename Event> void process_event(const Event &event)
{
    std::cout << "count = " << event.count << std::endl;
    std::cout << "value = " << event.value << std::endl;
    if constexpr (std::is_same_v<Event, EventB>)
    {
        std::cout << "rate = " << event.rate << std::endl;
    }
    else if constexpr (std::is_same_v<Event, EventC>)
    {
        std::cout << "label = " << event.label << std::endl;
    }
}

Здесь if constexpr (std::is_same_v<>) проверяет условие на этапе компиляции.

Если Event - это EventB, компилятор оставит ветку с rate.
Если Event - это EventC, оставит ветку с label.
А для EventA обе дополнительные ветки будут отброшены.

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

В runtime-подходе поведение обычно выбирается через switch, enum или виртуальные функции. В compile-time-подходе выбор происходит через типы, а неподходящие ветки просто не попадают в итоговую инстанциацию шаблона.

Type traits

В предыдущем примере мы проверяли конкретные типы напрямую:

std::is_same_v<Event, EventB>

Для небольшого примера это нормально. Код короткий, типов мало, всё легко держать в голове.

Но если система растёт, такие проверки быстро начинают засорять код. В обработчике появляется всё больше условий вида: «если это EventB», «если это EventC», «если это EventD». В итоге функция начинает знать слишком много о конкретных типах.

Для решения этого можно вынести описание свойств типа в отдельный traits:

template <typename Event> 
struct event_traits;

А дальше прост описать свойства для каждого события:

template <> struct event_traits<EventA>
{
    static constexpr std::string_view name = "event_a";
    static constexpr bool has_rate = false;
    static constexpr bool has_label = false;
};
template <> struct event_traits<EventB>
{
    static constexpr std::string_view name = "event_b";
    static constexpr bool has_rate = true;
    static constexpr bool has_label = false;
};
template <> struct event_traits<EventC>
{
    static constexpr std::string_view name = "event_c";
    static constexpr bool has_rate = false;
    static constexpr bool has_label = true;
};

Теперь обработчик можно написать чуть чище:

template <typename Event> void process_v2(const Event &event)
{
    using traits = event_traits<Event>;
    std::cout << "type = " << traits::name << '\n';
    std::cout << "count = " << event.count << '\n';
    std::cout << "value = " << event.value << '\n';
    if constexpr (traits::has_rate)
    {
        std::cout << "rate = " << event.rate << '\n';
    }
    if constexpr (traits::has_label)
    {
        std::cout << "label = " << event.label << '\n';
    }
}

Здесь обработчик уже не привязан напрямую к EventB или EventC. Он работает не с именами конкретных типов, а с их свойствами.

Это делает код гибче. Например, если появится новый тип события:

struct EventD {
    std::uint64_t count = 0;
    double value = 0.0;
    double rate = 0.0;
    double deviation = 0.0;
};

мы можем просто описать его свойства:

template <>
struct event_traits<EventD> {
    static constexpr std::string_view name = "event_d";
    static constexpr bool has_rate = true;
    static constexpr bool has_label = false;
};

И общий обработчик продолжит работать без изменений.

Type erasure

Это компромисс между template и virtual. Шаблоны хороши, когда конкретный тип известен на этапе компиляции. Но иногда нужно хранить разные реализации в одном контейнере или выбирать поведение в runtime. Можно использовать классический виртуальный интерфейс:

struct IProcessor {
    virtual void process(double value) = 0;
    virtual ~IProcessor() = default;
};

Но есть другой подход - type erasure, он позволяет спрятать конкретный тип за единым интерфейсом, не заставляя сам тип наследоваться от базового класса.

Простой пример:

class AnyProcessor
{
  public:
    template <typename Processor>
    AnyProcessor(Processor processor) : object_(std::make_shared<Model<Processor>>(std::move(processor))){}
    void process(double value) { object_->process(value); }

  private:
    struct Concept
    {
        virtual void process(double value) = 0;
        virtual ~Concept() = default;
    };

    template <typename Processor> 
    struct Model final : Concept
    {
        explicit Model(Processor processor) : processor_(std::move(processor)) {}

        void process(double value) override { processor_.process(value); }

        Processor processor_;
    };

    std::shared_ptr<Concept> object_;
};

Теперь конкретные типы не обязаны наследоваться от общего интерфейса:

struct LoggingProcessor
{
    void process(double value) { std::cout << "value = " << value << std::endl; }
};

struct CountingProcessor
{
    std::size_t count = 0;

    void process(double) { ++count; }
};

Их можно хранить единообразно:

std::vector<AnyProcessor> processors;

processors.emplace_back(LoggingProcessor{});
processors.emplace_back(CountingProcessor{});

for (auto& processor : processors) {
    processor.process(42.0);
}

Код может показаться на первый взгляд трудным, но что здесь произошло:

  • LoggingProcessor и CountingProcessor не знают про AnyProcessor

  • им не нужен общий базовый класс

  • внешний код работает с единым типом AnyProcessor

  • внутри используется runtime dispatch, но он локализован

Когда лучше virtual, когда template, а когда type erasure

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

Но внутри, где код выполняется часто и важна производительность, обычно лучше оставлять шаблоны. Там компилятор видит конкретные типы и может лучше оптимизировать код.

Поэтому здесь важно не противопоставлять эти подходы, а понимать, где каждый из них уместен.

Шаблоны дают compile-time polymorphism(статический полиморфизм): тип известен на этапе компиляции, и компилятор может собрать специализированный код под конкретный случай.

Виртуальные функции дают runtime polymorphism(динамический полиморфизм): конкретная реализация выбирается во время выполнения через общий базовый интерфейс.

Type erasure находится где-то между ними. Он тоже даёт runtime-гибкость, но при этом не заставляет пользовательские типы наследоваться от базового класса. Мы просто прячем конкретный тип за общей обёрткой.

Упрощённо:
template - выбор типа в compile-time, максимум оптимизации
virtual - выбор реализации в runtime через общий базовый класс
type erasure - runtime-обёртка без наследования в пользовательских типах

То есть практическое правило можно сформулировать так: внутри горячего пути — шаблоны, на стабильных runtime-границах — virtual, а там, где хочется runtime-гибкости без обязательного наследования, — type erasure.

Подведем итог:
Метапрограммирование в C++ действительно может превратиться в моветон, если использовать его ради самого метапрограммирования. Когда шаблоны появляются только для того, чтобы показать знание языка, код часто становится сложнее, а не лучше.

Но в правильном месте шаблоны решают вполне практические задачи. Они позволяют перенести часть решений на этап компиляции, убрать лишние runtime-проверки, описать требования к типам через concepts, собрать поведение из policy-типов и написать обобщённый код без лишней потери производительности.

Я не буду спорить, что у шаблонов есть цена, они могут увеличивать время компиляции, раздувать бинарник, усложнять ошибки, отладку и т.д.
Поэтому использовать их стоит не везде, а там, где тип действительно несёт смысл.

Жуков Матвей /НИУ МЭИ ИВТИ /Кафедра управления и интеллектуальных технологий

C++ Разработчик