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

SFINAE иand Concepts

SFINAE — это побочный эффект правил вывода шаблонов, превращённый в инструмент программирования. Concepts (C++20) — тот же результат (ограничить, какие типы годятся для шаблона), но выраженный как язык описывает намерение, а не как эксплуатация случайности компилятора.SFINAE is a side effect of template deduction rules, turned into a programming tool. Concepts (C++20) achieve the same result (constraining which types a template accepts), but expressed as the language stating intent, rather than exploiting a compiler accident.

C++20: ConceptsC++20: Concepts частый вопросcommon interview question

ОпределениеDefinition

SFINAE (Substitution Failure Is Not An Error) — правило, по которому если подстановка выведенных или явно заданных шаблонных аргументов в сигнатуру функции приводит к некорректному коду, компилятор не выдаёт ошибку, а просто исключает эту перегрузку из рассмотрения, как если бы её не существовало. Concepts (C++20) — именованные предикаты компиляции над типами, которые ограничивают, какие типы допустимы для параметра шаблона, и дают читаемую ошибку прямо на месте вызова, а не глубоко внутри тела шаблона.SFINAE (Substitution Failure Is Not An Error) is the rule that if substituting deduced or explicit template arguments into a function's signature produces invalid code, the compiler doesn't error out — it simply drops that overload from consideration, as if it never existed. Concepts (C++20) are named compile-time predicates over types that constrain which types a template parameter accepts, and give a readable error right at the call site instead of deep inside the template body.

// SFINAE: has_size выбирается, только если T.size() существует// SFINAE: has_size is only picked if T.size() exists
template<typename T>
auto describe(const T& x) -> decltype(x.size(), void()) {
    std::cout << "has size: " << x.size();
}

// Concepts: то же самое, но читается как требование, а не трюк// Concepts: the same thing, but reads as a requirement, not a trick
template<typename T>
concept HasSize = requires(T x) { x.size(); };

template<HasSize T>
void describe(const T& x) {
    std::cout << "has size: " << x.size();
}

Как работает SFINAEHow SFINAE works

Ключевое слово — "substitution failure", а не "instantiation failure": SFINAE применяется только к ошибкам, возникающим при подстановке аргументов в объявление шаблона (тип возврата, типы параметров, список шаблонных аргументов по умолчанию). Ошибка внутри ТЕЛА функции — уже не substitution failure, а настоящая ошибка компиляции, потому что тело проверяется только после того, как перегрузка выбрана и инстанцирована.The key word is "substitution failure," not "instantiation failure": SFINAE applies only to errors that arise when substituting arguments into the template's declaration (return type, parameter types, default template argument list). An error inside the function's BODY is no longer a substitution failure — it's a genuine compile error, because the body is only checked after an overload has been selected and instantiated.

Классический ручной инструмент — std::enable_if: он определяет вложенный тип ::type только если условие истинно; если условие ложно, ::type не существует, подстановка в сигнатуру проваливается, и перегрузка молча исключается.The classic hand-rolled tool is std::enable_if: it defines a nested ::type only when the condition is true; if the condition is false, ::type doesn't exist, substitution into the signature fails, and the overload is silently dropped.

template<typename T>
typename std::enable_if<std::is_integral_v<T>, T>::type
half(T x) { return x / 2; }             // только для целых типов// only for integral types

template<typename T>
typename std::enable_if<std::is_floating_point_v<T>, T>::type
half(T x) { return x * 0.5; }          // только для плавающей точки// only for floating point

// half(5) выбирает первую, half(5.0) — вторую; half("x") — обе проваливают SFINAE, ошибка "нет подходящей перегрузки"// half(5) picks the first, half(5.0) the second; half("x") fails SFINAE for both, error "no matching overload"

Проблема этого подхода — сообщения об ошибках. Если ни одна перегрузка не подходит, компилятор выдаёт длинный список "candidate template ignored: substitution failure" без указания, ПОЧЕМУ именно — приходится вручную разбирать цепочку enable_if.The problem with this approach is error messages. If no overload matches, the compiler emits a long list of "candidate template ignored: substitution failure" without saying WHY — you have to manually untangle the enable_if chain yourself.

Concepts: то же самое ограничение, выраженное как требованиеConcepts: the same constraint, expressed as a requirement

requires-выражение проверяет, что заданный набор выражений корректно компилируется для данного типа — это формализация того самого трюка с decltype(x.size(), void()), но с читаемым синтаксисом и читаемой ошибкой.A requires-expression checks that a given set of expressions compiles correctly for a type — a formalization of exactly that decltype(x.size(), void()) trick, but with readable syntax and a readable error.

template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

template<Numeric T>                       // сокращённый синтаксис — concept вместо typename// abbreviated syntax — concept instead of typename
T half(T x) { return x / 2; }

template<typename T>
requires Numeric<T>                  // та же мысль, требование отдельной строкой// same idea, requirement as a separate clause
T half2(T x) { return x / 2; }

template<typename T>
requires requires(T a, T b) { { a + b } -> std::same_as<T>; }  // ad-hoc requires без именованного concept// an ad-hoc requires with no named concept
T sum2(T a, T b) { return a + b; }

// half("x") -> ошибка прямо здесь: "constraints not satisfied: Numeric<const char*> evaluated to false"// half("x") -> error right here: "constraints not satisfied: Numeric<const char*> evaluated to false"

Ключевое отличие в диагностике: ошибка concept указывает, какое ИМЕННО требование не выполнено (Numeric<const char*> evaluated to false) прямо в точке вызова — не нужно вручную разворачивать enable_if. Стандартная библиотека определяет готовый набор concepts в <concepts>: std::integral, std::floating_point, std::same_as, std::convertible_to, std::copyable и другие.The key difference is diagnostics: a concept error states exactly WHICH requirement failed (Numeric<const char*> evaluated to false) right at the call site — no need to manually unwind an enable_if chain. The standard library defines a ready set of concepts in <concepts>: std::integral, std::floating_point, std::same_as, std::convertible_to, std::copyable, and others.

Concepts как компонент выбора перегрузкиConcepts as a piece of overload resolution

Помимо диагностики, concepts участвуют в разрешении перегрузок как формальный критерий: если несколько шаблонов подходят по типу, но один ограничен более специфичным concept-ом, а другой — более общим (concept-подмножество), компилятор выбирает более ограниченный (partial ordering of constraints) — раньше для того же эффекта требовалась ручная специализация или тэг-диспетчеризация.Beyond diagnostics, concepts participate in overload resolution as a formal criterion: if several templates match by type but one is constrained by a more specific concept and another by a more general one (a concept subset), the compiler picks the more constrained one (partial ordering of constraints) — previously this same effect required manual specialization or tag dispatch.

template<typename T>
concept Integral = std::is_integral_v<T>;
template<typename T>
concept SignedIntegral = Integral<T> && std::is_signed_v<T>;

void process(Integral auto x)       { std::cout << "integral"; }
void process(SignedIntegral auto x) { std::cout << "signed integral"; }  // более специфичный — выигрывает для int// more specific — wins for int

ГраблиGotchas

SFINAE применяется только к immediate context, не к телуSFINAE applies only to the immediate context, not the body

Ошибка глубоко внутри вложенного шаблонного вызова, а не непосредственно в сигнатуре, — не substitution failure, а настоящий hard error, даже если формально она произошла "во время подстановки". Это различие ("immediate context") — источник тонких багов, когда SFINAE-трюк не срабатывает так, как ожидалось.An error deep inside a nested template call, rather than directly in the signature, is not a substitution failure — it's a genuine hard error, even though it technically happened "during substitution." This distinction (the "immediate context" rule) is a source of subtle bugs when an SFINAE trick doesn't fire as expected.

Concepts не заменяют static_assert внутри телаConcepts don't replace a static_assert inside the body

Concept на шаблонном параметре ограничивает, какие типы допускаются к ВЫЗОВУ функции, но не проверяет ничего внутри тела заново — если тело использует операцию, не покрытую concept-ом, ошибка компиляции всё равно будет глубоко в теле, просто для меньшего числа типов, чем без concept-а вовсе.A concept on a template parameter constrains which types can CALL the function, but doesn't separately re-check anything inside the body — if the body uses an operation the concept doesn't cover, the compile error still surfaces deep in the body, just for fewer types than without the concept at all.

void_t — идиома для детектирования произвольных выражений до Concepts— the idiom for detecting arbitrary expressions before Concepts

До C++20 for обнаружения "есть ли у типа метод X" использовался идиоматический std::void_t в связке с частичной специализацией — более громоздкий, но эквивалентный по мощи requires-выражению. Знание этой идиомы всё ещё полезно для чтения legacy-кода на C++11/14/17.Before C++20, detecting "does this type have method X" used the idiomatic std::void_t combined with partial specialization — more verbose, but equivalent in power to a requires-expression. Knowing this idiom is still useful for reading legacy C++11/14/17 code.