concepts и rangesconcepts and ranges
Именованное, читаемое ограничение на шаблонный параметр — template<std::integral T> T add(T a, T b); вместо страницы SFINAE ради того же результата.A named, readable constraint on a template parameter — template<std::integral T> T add(T a, T b); instead of a page of SFINAE to achieve the same result.
Concept — именованный предикат на этапе компиляции, накладывающий требования на шаблонный параметр: какие операции должны быть валидны, каким другим concept'ам должен удовлетворять тип, какие типы должны существовать. Concept объявляется через concept, обычно на основе requires-выражения, и применяется как ограничение шаблона — либо вместо class/typename, либо в отдельном requires-предложении.A concept is a named, compile-time predicate that places requirements on a template parameter: which operations must be valid, which other concepts the type must satisfy, which nested types must exist. A concept is declared with concept, usually built on a requires-expression, and applied as a template constraint — either in place of class/typename, or in a separate requires clause.
template <class T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
T add(T a, T b) { return a + b; }
// ошибка при неподходящем типе — нечитаемая простыня из шаблонов// error on a bad type — an unreadable wall of template internals
template <std::integral T>
T add(T a, T b) { return a + b; }
// ошибка при неподходящем типе — понятное сообщение "T does not satisfy integral"// error on a bad type — a clear "T does not satisfy integral" message
SFINAE решала задачу ограничения шаблонов с самого начала generic-программирования в C++, но ценой читаемости и качества диагностики:SFINAE had solved the template-constraint problem since the earliest days of generic programming in C++, but at the cost of readability and diagnostic quality:
Когда шаблон, ограниченный через enable_if, не подходит для типа, компилятор выдаёт страницы вложенных сообщений о том, что подстановка не удалась где-то в глубине служебных метафункций — сама причина (например, "тип не integral") тонет в шуме реализации ограничения, а не самого требования.When a template constrained via enable_if doesn't match a type, the compiler produces pages of nested substitution-failure messages buried deep inside helper metafunctions — the actual reason (say, "the type isn't integral") gets lost in the noise of how the constraint was implemented, not what it actually required.
Каждое использование одного и того же логического ограничения ("тип должен быть сравним на равенство и хешируем") требовало заново выписывать enable_if-выражение целиком — не было способа дать этому ограничению имя и переиспользовать его в десятке шаблонов, как обычную функцию.Every use of the same logical constraint ("the type must be equality-comparable and hashable") required re-writing the whole enable_if expression from scratch — there was no way to give that constraint a name and reuse it across a dozen templates, the way an ordinary function can be reused.
Две перегрузки одной функции, ограниченные разными enable_if-условиями, могли неожиданно оказаться неоднозначными или, наоборот, не находиться компилятором вовсе — правила частичного упорядочивания шаблонов не были рассчитаны на такой стиль ограничений и вели себя непредсказуемо в пограничных случаях.Two overloads of the same function, constrained by different enable_if conditions, could unexpectedly become ambiguous, or conversely fail to be found by the compiler at all — the rules for partial template ordering weren't designed around this style of constraint and behaved unpredictably at the edges.
Concept — это переменная-шаблон особого вида (constexpr bool, определяемый на этапе компиляции), проверяющая, что requires-выражение истинно для данного набора типов. Requires-выражение перечисляет требования — валидные выражения, требуемые типы, требуемые compile-time условия — и вычисляется в true/false без выполнения перечисленного кода, только проверкой его валидности.A concept is a special kind of variable template (constexpr bool, evaluated at compile time) that checks whether a requires-expression holds for a given set of types. A requires-expression lists requirements — valid expressions, required nested types, required compile-time conditions — and evaluates to true/false without executing the listed code, only checking its validity.
template <class T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // выражение a+b должно быть валидно// the expression a+b must be valid
// и приводимо к T// and convertible to T
};
template <Addable T> // concept вместо class/typename — ограничение прямо в объявлении// concept instead of class/typename — the constraint lives right in the declaration
T sum(T a, T b) {
return a + b;
}
// эквивалентная форма с отдельным requires-предложением:// an equivalent form with a separate requires clause:
template <class T>
requires Addable<T>
T sum2(T a, T b) { return a + b; }
Ключевое отличие от SFINAE: при неудаче проверки concept компилятор точно знает, какое именно требование не выполнено (например, конкретно "выражение a + b не валидно для этого T"), и сообщает об этом напрямую — вместо того чтобы репортить провал подстановки где-то в недрах вспомогательной метафункции.The key difference from SFINAE: when a concept check fails, the compiler knows exactly which requirement wasn't met (for instance, specifically "the expression a + b is not valid for this T"), and reports that directly — instead of reporting a substitution failure buried somewhere inside a helper metafunction.
Requires-выражение поддерживает четыре вида требований одновременно — простые (выражение валидно), составные (выражение валидно и его результат удовлетворяет ограничению), типовые (существует такой вложенный тип) и вложенные (произвольное compile-time условие):A requires-expression supports four kinds of requirements at once — simple (an expression is valid), compound (an expression is valid and its result satisfies a constraint), type (a nested type exists), and nested (an arbitrary compile-time condition):
template <class T>
concept Container = requires(T c) {
typename T::value_type; // требование типа — должен существовать T::value_type// type requirement — T::value_type must exist
c.begin(); // простое требование — выражение должно быть валидно// simple requirement — the expression must be valid
c.end();
{ c.size() } -> std::convertible_to<std::size_t>; // составное требование// compound requirement
requires std::default_initializable<T>; // вложенное требование — ссылка на другой concept// nested requirement — referencing another concept
};
// составление нескольких concepts через && — обычное булево выражение:// composing several concepts with && — an ordinary boolean expression:
template <class T>
concept SortableContainer = Container<T> && std::totally_ordered<typename T::value_type>;
Функция сериализации в бинарный протокол, которая должна принимать только числа (не строки, не указатели) — раньше делалось через enable_if с is_arithmetic, теперь читается как обычная сигнатура.A binary-protocol serialization function that should accept only numbers (not strings, not pointers) — used to be enable_if with is_arithmetic, now reads like an ordinary signature.
template <std::floating_point T>
void write_price(std::ostream& out, T value) {
char buf[32];
auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), value);
out.write(buf, ptr - buf);
}
До concepts выбор между алгоритмом для random-access и forward итераторов делался через tag dispatch (пустые struct-теги + перегрузка по ним). С concepts — прямое ограничение на сигнатуре.Before concepts, choosing between an algorithm for random-access and forward iterators was done via tag dispatch (empty struct tags plus overloading on them). With concepts — a direct constraint on the signature.
template <std::random_access_iterator It>
void advance_fast(It& it, std::ptrdiff_t n) { it += n; } // O(1)// O(1)
template <std::forward_iterator It>
void advance_fast(It& it, std::ptrdiff_t n) {
while (n--) ++it; // O(n), но компилируется для любого forward-итератора// O(n), but compiles for any forward iterator
}
Функция, работающая с любым типом точки (своя структура, структура из внешней библиотеки, structured binding-совместимый tuple) без наследования от общего базового класса.A function that works with any point-like type (your own struct, a struct from an external library, a structured-binding-compatible tuple) without inheriting from a shared base class.
template <class P>
concept Point2D = requires(P p) {
{ p.x } -> std::convertible_to<double>;
{ p.y } -> std::convertible_to<double>;
};
template <Point2D P>
double distance(P a, P b) {
return std::hypot(a.x - b.x, a.y - b.y);
}
Метод шаблонного класса, доступный только для типов, поддерживающих сравнение — например, метод sort() контейнера, инстанциируемого и для несравнимых типов, но с sort() под ограничением.A method of a template class available only for types that support comparison — for instance, a container's sort() method, where the container is instantiable for non-comparable types too, but sort() itself is constrained.
template <class T>
class Bucket {
std::vector<T> items_;
public:
void add(T v) { items_.push_back(std::move(v)); }
void sort() requires std::totally_ordered<T> { // доступен, только если T сравним// available only if T is comparable
std::ranges::sort(items_);
}
};
Concept проверяет, что операция валидна с точки зрения компиляции (выражение компилируется, тип существует) — он не проверяет, что операция ведёт себя ожидаемым образом семантически. Тип с перегруженным operator+, который на самом деле вычитает, формально удовлетворит concept Addable из примера выше — concepts документируют синтаксический контракт, не гарантируют корректность поведения.A concept checks that an operation is valid from the compiler's point of view (the expression compiles, the type exists) — it does not check that the operation behaves as semantically expected. A type with an overloaded operator+ that actually subtracts will formally satisfy the Addable concept from the example above — concepts document a syntactic contract, they don't guarantee correct behavior.
Если две перегрузки применимы одновременно, компилятор выбирает ту, чей concept логически подразумевает concept другой (subsumption) — но subsumption вычисляется по структуре булевых выражений внутри concept, а не по «интуитивной» вложенности требований. Composить concepts через &&/|| напрямую внутри определения (а не через ссылку на другой именованный concept) иногда ломает ожидаемое отношение subsumption.If two overloads are simultaneously viable, the compiler picks the one whose concept logically subsumes the other's — but subsumption is computed from the boolean structure of the concept's definition, not from the "intuitive" nesting of requirements. Composing concepts with &&/|| directly inline (rather than by referencing another named concept) sometimes breaks the expected subsumption relationship.
Concepts ограничивают именно шаблонные параметры на этапе вывода типов и разрешения перегрузок — они не заменяют static_assert внутри тела функции для проверки условий, не связанных напрямую с типом параметра (см. cpp11/static-assert.html). Оба механизма сосуществуют и решают разные задачи.Concepts specifically constrain template parameters during type deduction and overload resolution — they don't replace a static_assert inside a function body for checking conditions unrelated to the parameter's type (see cpp11/static-assert.html). Both mechanisms coexist and solve different problems.