шаблоны и метапрограммированиеtemplates and metaprogramming

Инстанцирование шаблоновTemplate instantiation

Шаблон — это не код, а рецепт кода. Инстанцирование — момент, когда компилятор подставляет конкретные типы и генерирует настоящую функцию/класс. Понимание того, КОГДА и КАК это происходит, объясняет и время компиляции, и раздувание бинарника, и почему шаблоны обычно живут в заголовках.A template isn't code — it's a recipe for code. Instantiation is the moment the compiler substitutes concrete types and generates an actual function/class. Understanding WHEN and HOW this happens explains compile time, binary bloat, and why templates usually live in headers.

фундаментfoundational частый вопросcommon interview question

Вывод типов (template argument deduction)Type deduction

Компилятор выводит шаблонные аргументы из типов реальных аргументов вызова, сравнивая тип параметра с типом аргумента "структурно". Ссылки и cv-квалификаторы отбрасываются по определённым правилам, что часто удивляет новичков.The compiler deduces template arguments from the types of the actual call arguments, matching the parameter type against the argument type "structurally." References and cv-qualifiers are dropped according to specific rules, which often surprises newcomers.

template<typename T>
void byValue(T x);        // T выводится без ссылок и верхнего const// T is deduced with references and top-level const stripped

template<typename T>
void byRef(T& x);         // T сохраняет const, если аргумент — const-lvalue// T preserves const if the argument is a const lvalue

template<typename T>
void forward(T&& x);       // универсальная (forwarding) ссылка: T = U& для lvalue, T = U для rvalue// universal (forwarding) reference: T = U& for an lvalue, T = U for an rvalue

const int ci = 10;
byValue(ci);   // T = int (const отброшен — копия не обязана быть const)// T = int (const dropped — the copy needn't be const)
byRef(ci);     // T = const int (ссылка должна уважать константность источника)// T = const int (a reference must respect the source's constness)
int x = 5;
forward(x);    // x — lvalue, T выводится как int&, схлопывание ссылок даёт T&& = int&// x is an lvalue, T deduces to int&, reference collapsing gives T&& = int&
forward(5);    // 5 — rvalue, T выводится как int, T&& = int&&// 5 is an rvalue, T deduces to int, T&& = int&&

Правило схлопывания ссылок (reference collapsing: & &, & &&, && & → &; только && && → &&) — механизм, на котором держится std::forward и вся идея perfect forwarding: forwarding-ссылка "запоминает" категорию значения исходного аргумента и передаёт её дальше без лишнего копирования.The reference collapsing rule (& &, & &&, && & → &; only && && → &&) is the mechanism that std::forward and the entire perfect-forwarding idea rest on: a forwarding reference "remembers" the original argument's value category and passes it along without an unnecessary copy.

Неявное инстанцирование: по требованию, ленивоImplicit instantiation: on demand, lazily

Компилятор генерирует код шаблона только тогда, когда он реально используется с конкретными типами (point of instantiation) — не при определении шаблона. Это объясняет, почему ошибки в неиспользуемых ветках шаблонного кода (например, в if constexpr с ложным условием для данного T, или просто в неиспользуемом методе шаблонного класса) не всплывают, пока код действительно не будет инстанцирован.The compiler generates a template's code only when it's actually used with concrete types (the point of instantiation) — not at the template's definition. This explains why errors in unused branches of template code (e.g., inside an if constexpr whose condition is false for this T, or simply an unused method of a template class) don't surface until the code is actually instantiated.

template<typename T>
struct Box {
    T value;
    void onlyForInt() { value.nonexistent_method(); }  // компилируется, пока не вызван// compiles fine as long as it's never called
};
Box<int> b{42};   // Box<int> инстанцируется, но onlyForInt() — нет (метод не вызван)// Box<int> is instantiated, but onlyForInt() isn't (never called)

Каждая единица трансляции (.cpp-файл), использующая шаблон с определённым набором типов, инстанцирует его заново независимо — отсюда требование ODR (one definition rule) с оговоркой "определения шаблонов могут повторяться в разных единицах трансляции при условии идентичности", и отсюда же необходимость линкера "схлопывать" (deduplicate) одинаковые инстанциации, если шаблон определён в заголовке.Every translation unit (.cpp file) that uses a template with a given set of types instantiates it independently — hence the ODR (one definition rule) carve-out "template definitions may repeat across translation units provided they're identical," and hence the linker's need to deduplicate identical instantiations when a template is defined in a header.

Явная специализация и явное инстанцированиеExplicit specialization and explicit instantiation

Явная специализация (template<> ...) — предоставление полностью отдельной реализации шаблона для конкретного типа, вместо использования общего рецепта. Частичная специализация — то же самое, но для подмножества типов, а не для одного конкретного (доступна только для классов, не для функций).Explicit specialization (template<> ...) provides a wholly separate implementation of the template for a specific type, instead of using the general recipe. Partial specialization is the same idea, but for a subset of types rather than one specific type (available for classes only, not functions).

template<typename T>
struct TypeName { static constexpr auto value = "unknown"; };

template<>                                    // полная специализация для int// full specialization for int
struct TypeName<int> { static constexpr auto value = "int"; };

template<typename T>                    // частичная специализация для любого указателя// partial specialization for any pointer
struct TypeName<T*> { static constexpr auto value = "pointer"; };

Явное инстанцирование (template class Box<int>; вне класса) заставляет компилятор сгенерировать код сразу, в конкретной единице трансляции, независимо от того, используется ли он там — классический способ сократить время компиляции для широко переиспользуемых шаблонов: определение остаётся в .cpp, а не в заголовке, а явные инстанциации для нужных типов подключаются через отдельный .cpp, скомпилированный один раз.Explicit instantiation (template class Box<int>; outside the class) forces the compiler to generate the code right away, in a specific translation unit, regardless of whether it's used there — a classic technique for cutting compile time for widely-reused templates: the definition stays in a .cpp file instead of a header, and explicit instantiations for the needed types are compiled once in a separate .cpp.

ГраблиGotchas

Шаблоны в .cpp без явного инстанцирования — ошибка линковки, а не компиляцииTemplates in a .cpp without explicit instantiation — a link error, not a compile error

Если определение шаблонной функции лежит в .cpp (не в заголовке) и не инстанцировано явно, вызов из другой единицы трансляции скомпилируется (объявление видно), но не слинкуется (undefined reference) — компилятор не смог инстанцировать шаблон, потому что не видел его тело в момент компиляции вызывающего кода. Отсюда правило "шаблоны живут в заголовках".If a template function's definition lives in a .cpp (not a header) and isn't explicitly instantiated, a call from another translation unit will compile (the declaration is visible) but fail to link (undefined reference) — the compiler couldn't instantiate the template because it never saw its body while compiling the caller. Hence the rule "templates live in headers."

Class Template Argument Deduction (CTAD) — не всегда работает без deduction guideClass Template Argument Deduction (CTAD) doesn't always work without a deduction guide

std::pair p(1, 2.0);          // C++17: CTAD выводит pair<int, double> из конструктора// C++17: CTAD deduces pair<int, double> from the constructor

template<typename T>
struct Wrapper { Wrapper(T*); };
// Wrapper w(ptr); может не вывести T ожидаемо без явного deduction guide:// Wrapper w(ptr); may not deduce T as expected without an explicit deduction guide:
template<typename T> Wrapper(T*) -> Wrapper<T>;

Раздувание бинарника (code bloat) — цена неявного инстанцированияBinary bloat is the price of implicit instantiation

Каждая уникальная комбинация типов шаблона — отдельная копия скомпилированного кода. std::vector<int> и std::vector<double> — два независимых набора функций в бинарнике, даже если логика идентична. Линкер умеет схлопывать идентичные инстанциации между единицами трансляции (COMDAT folding), но не между РАЗНЫМИ типами.Every unique combination of template types is a separate copy of compiled code. std::vector<int> and std::vector<double> are two independent sets of functions in the binary, even though the logic is identical. The linker can fold identical instantiations across translation units (COMDAT folding), but not across DIFFERENT types.