concepts и rangesconcepts and ranges

<concepts>

Готовый словарь общеупотребительных ограничений — std::integral, std::same_as, std::invocable — не нужно объявлять свой concept для того, что уже придумано и стандартизировано.A ready-made vocabulary of common constraints — std::integral, std::same_as, std::invocable — no need to declare your own concept for something already invented and standardized.

чистая библиотека, не языковая фичаa pure library, not a language feature частый вопрос: чем std::regular отличается от std::semiregularcommon question: how std::regular differs from std::semiregular

Что этоWhat it is

Заголовок <concepts> — набор готовых concepts на самые частые ограничения шаблонных параметров, сгруппированных по смыслу: базовые отношения между типами, сравнения, арифметические категории, объектные свойства (копируемость, движимость, регулярность) и вызываемость. Все они — обычные библиотечные concepts, объявленные через тот же механизм concept/requires, что описан в concepts — синтаксисе языка — никакой отдельной магии компилятора для них нет.The <concepts> header is a set of ready-made concepts for the most common template-parameter constraints, grouped by meaning: basic relationships between types, comparisons, arithmetic categories, object properties (copyability, movability, regularity), and callability. All of them are ordinary library concepts, declared with the same concept/requires machinery covered in concepts — language syntax — there's no separate compiler magic behind them.

группаgroupконцептыconceptsсмыслmeaning
базовыеbasicstd::same_as<T,U>, std::derived_from<D,B>, std::convertible_to<From,To>отношения между двумя типамиrelationships between two types
сравненияcomparisonsstd::equality_comparable, std::totally_orderedподдержка ==/!= и полного упорядочиванияsupport for ==/!= and a total ordering
арифметическиеarithmeticstd::integral, std::floating_point, std::signed_integralпринадлежность к числовой категорииmembership in a numeric category
объектныеobjectstd::movable, std::copyable, std::semiregular, std::regularжизненный цикл объекта — конструирование, копирование, сравнениеobject lifecycle — construction, copying, comparison
вызываемыеcallablestd::invocable<F, Args...>, std::predicateобъект можно вызвать с данными аргументамиthe object can be called with these arguments

Зачем ввелиWhy it was introduced

Сам язык мог дать только механизм concept/requires — но без стандартного набора готовых concepts каждый проект заново изобретал бы одни и те же ограничения:The language itself could only provide the concept/requires mechanism — but without a standard set of ready-made concepts, every project would keep reinventing the same constraints:

01

Одни и те же ограничения писались заново в каждой кодовой базеThe same constraints were rewritten in every codebase

"Тип должен быть числом", "тип должен быть сравним", "тип должен быть копируемым и умолчательно конструируемым" — до <concepts> это выражалось либо через enable_if, либо через самописный concept с произвольным именем, разным в каждом проекте."The type must be a number," "the type must be comparable," "the type must be copyable and default-constructible" — before <concepts>, this was expressed either via enable_if, or via a hand-rolled concept with an arbitrary name, different in every project.

02

Разные имена для одного и того же требования мешали переносу кода и пониманиюDifferent names for the same requirement hurt code portability and understanding

Концепт std::integral<T> не нуждается в комментарии — имя говорит само за себя и означает одно и то же в любом проекте на C++20. Самописный IsIntegerLike<T> нуждается: непонятно, эквивалентен ли он is_integral, что именно проверяет и почему назван иначе. Стандартизация даёт общий словарь имён поверх всех кодовых баз.The concept std::integral<T> needs no comment — the name speaks for itself and means the same thing in any C++20 project. A hand-rolled IsIntegerLike<T> does need one: it's unclear whether it's equivalent to is_integral, what exactly it checks, and why it's named differently. Standardization gives a shared vocabulary of names across codebases.

03

Ranges и другие новые части библиотеки требовали фундамента из готовых conceptsRanges and other new library parts needed a foundation of ready-made concepts

<ranges> и обновлённый <algorithm> (см. ranges — основы) сами построены поверх concepts — std::ranges::sortable внутри опирается на std::movable и std::totally_ordered. Библиотека готовых concepts — не факультативное удобство, а необходимая инфраструктура для остального C++20.Both <ranges> and the updated <algorithm> (see ranges — basics) are themselves built on top of concepts — std::ranges::sortable relies internally on std::movable and std::totally_ordered. The library of ready-made concepts isn't an optional convenience, it's necessary infrastructure for the rest of C++20.

Как это работает под капотомHow it works under the hood

Большинство concepts из <concepts> — это тонкие обёртки над type traits из <type_traits>: например, std::integral<T> по сути равносилен std::is_integral_v<T>, только выражен как concept, а не как булева переменная-шаблон. Более сложные concepts комбинируют требования через requires-выражения и через ссылки на другие, более мелкие concepts — composable concepts, собранные логическим "и".Most concepts in <concepts> are thin wrappers over type traits from <type_traits> — for instance, std::integral<T> is effectively equivalent to std::is_integral_v<T>, just expressed as a concept rather than a boolean variable template. More elaborate concepts combine requirements through requires-expressions and by referencing other, smaller concepts — composable concepts assembled with logical AND.

// приблизительно так выглядит std::integral изнутри стандартной библиотеки// roughly what std::integral looks like inside the standard library
template <class T>
concept integral = std::is_integral_v<T>;

// composable concept: собран из более мелких concepts через &&// a composable concept: assembled from smaller concepts with &&
template <class T>
concept Regular = std::copyable<T>
    && std::default_initializable<T>
    && std::equality_comparable<T>;

// использование — ничем не отличается от самописного concept// usage — no different from a hand-rolled concept
template <std::invocable<int> F>
void apply(F f) { f(42); }

Это и есть std::regular в упрощённом виде — стандартная библиотека определяет его именно как комбинацию copyable + default_initializable + equality_comparable, а не как отдельную сущность со своей проверкой с нуля.This is effectively a simplified std::regular — the standard library defines it exactly as a combination of copyable + default_initializable + equality_comparable, not as a separate entity with its own from-scratch check.

Готовые concepts свободно комбинируются между собой через && прямо в requires-предложении — не обязательно заводить для этого отдельный именованный concept, если комбинация используется один раз. Тот же приём применим и к concepts высшего порядка вроде std::invocable/std::predicate, которые проверяют не свойство типа, а то, что объект можно вызвать с конкретным набором аргументов и получить ожидаемый результат.Ready-made concepts combine freely with && directly in a requires clause — there's no need to introduce a separate named concept for a combination used only once. The same trick applies to higher-order concepts like std::invocable/std::predicate, which check not a property of a type but that an object can be called with a specific set of arguments and produce the expected result.

// комбинация двух готовых concepts прямо в requires — без отдельного именованного concept// combining two ready-made concepts directly in requires — no separate named concept
template <class T>
requires std::integral<T> && std::signed_integral<T>
T abs_diff(T a, T b) {
    return a > b ? a - b : b - a;    // корректно только для знаковых целых// correct only for signed integers
}

// std::predicate — вызываемый объект, возвращающий нечто, приводимое к bool// std::predicate — a callable that returns something convertible to bool
template <class T, std::predicate<T> Pred>
std::size_t count_matching(std::vector<T>const& items, Pred pred) {
    std::size_t n = 0;
    for (const auto& item : items) if (pred(item)) ++n;
    return n;                       // вызов не int-специфичен: Pred может быть любым вызываемым для T// the call isn't int-specific: Pred can be anything callable on T
}

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Ограничение шаблонной функции сортировки через std::totally_orderedConstraining a template sort function with std::totally_ordered

Собственная реализация сортировки (например, insertion sort для маленьких массивов в гибридном алгоритме) должна работать только с типами, которые можно полностью упорядочить — сравнение через < должно быть валидно и согласовано с остальными операторами.A hand-rolled sort implementation (say, insertion sort for small arrays inside a hybrid algorithm) should only work with types that can be fully ordered — comparison via < must be valid and consistent with the rest of the comparison operators.

template <std::totally_ordered T>
void insertion_sort(std::vector<T>& v) {
    for (std::size_t i = 1; i < v.size(); ++i) {
        T key = v[i];
        std::size_t j = i;
        while (j > 0 && key < v[j - 1]) { v[j] = v[j - 1]; --j; }
        v[j] = key;
    }
}
02

Constraint на callback-параметр через std::invocableConstraining a callback parameter with std::invocable

Функция высшего порядка, принимающая произвольный вызываемый объект (функцию, лямбду, functor) с конкретной сигнатурой аргументов — ошибка в типе callback ловится в точке объявления шаблона, а не где-то внутри его тела.A higher-order function accepting an arbitrary callable (a function, a lambda, a functor) with a specific argument signature — a mismatched callback type is caught right at the template declaration, not somewhere deep inside its body.

template <std::invocable<int> F>
void for_each_tick(int count, F callback) {
    for (int i = 0; i < count; ++i) callback(i);  // callback обязан принимать int// callback must accept an int
}
03

Проверка типа через std::same_as внутри if constexpr перед статической диспетчеризациейChecking a type with std::same_as inside if constexpr before static dispatch

Шаблонная функция, ведущая себя по-разному в зависимости от конкретного типа параметра — например, разный формат сериализации для строк и чисел, выбираемый на этапе компиляции без рантайм-ветвления (см. if constexpr).A template function behaving differently depending on the exact parameter type — for instance, a different serialization format for strings and numbers, picked at compile time with no runtime branching (see if constexpr).

template <class T>
void serialize(std::ostream& out, Tconst& value) {
    if constexpr (std::same_as<T, std::string>) {
        out << '"' << value << '"';      // строки — в кавычках// strings — quoted
    } else if constexpr (std::integral<T>) {
        out << value;                       // числа — как есть// numbers — as-is
    }
}
04

std::regular как ограничение на тип-ключ в самописной hash-mapstd::regular as a constraint on the key type in a hand-rolled hash map

Хэш-таблица из cpp11/unordered-containers.html реализована на конкретном типе ключа без ограничений на этапе компиляции. Обобщённая версия того же контейнера может потребовать от параметра K ровно то, что нужно для корректной работы хэш-таблицы — конструируемость по умолчанию, копируемость, сравнение на равенство — одним именем вместо перечисления каждого требования по отдельности.The hash table from cpp11/unordered-containers.html is implemented for one concrete key type with no compile-time constraints. A generalized version of the same container can require exactly what a hash table needs from its K parameter — default-constructibility, copyability, equality comparison — under one name instead of listing every requirement separately.

template <std::regular K, class V>
class SimpleHashMap {
    // K обязан быть regular: default-constructible + copyable + equality-comparable —// K must be regular: default-constructible + copyable + equality-comparable —
    // именно то, что нужно бакетам хэш-таблицы для хранения и поиска ключей// exactly what the hash table's buckets need to store and look up keys
    std::vector<std::vector<std::pair<K, V>>> buckets_;
public:
    explicit SimpleHashMap(std::size_t bucket_count) : buckets_(bucket_count) {}
    // insert/find используют K{} и K == K, гарантированные std::regular// insert/find rely on K{} and K == K, guaranteed by std::regular
};

ГраблиGotchas

std::same_as<T,U> коммутативен — порядок аргументов не имеет значенияstd::same_as<T,U> is commutative — argument order doesn't matter

По построению std::same_as<T,U> и std::same_as<U,T> всегда дают одинаковый результат. Исторически concept реализован через два обращения к std::is_same (в обе стороны), чтобы формально гарантировать эту симметрию на уровне определения, а не полагаться на то, что is_same сам по себе уже симметричен. Это деталь реализации — не повод подозревать, что порядок аргументов может повлиять на результат.By construction, std::same_as<T,U> and std::same_as<U,T> always give the same result. Historically the concept is implemented via two calls to std::is_same (in both directions) to formally guarantee this symmetry at the definition level, rather than relying on is_same already being symmetric on its own. That's an implementation detail — not a reason to suspect argument order could affect the result.

std::regular/std::semiregular — строгие составные concepts, легко переограничить generic-кодstd::regular/std::semiregular are strict compound concepts — easy to over-constrain generic code

std::semiregular требует одновременно default-constructible и copyable; std::regular добавляет к этому ещё equality-comparable. Использовать их как ограничение для шаблона, которому на деле нужна только копируемость (без умолчательного конструктора или сравнения), излишне сужает набор допустимых типов — тип, не удовлетворяющий более строгому составному concept, не сможет использовать функцию, хотя формально ей было бы достаточно.std::semiregular requires both default-constructible and copyable simultaneously; std::regular adds equality-comparable on top of that. Using them as a constraint for a template that actually only needs copyability (without a default constructor or comparison) needlessly narrows the set of valid types — a type that doesn't satisfy the stricter compound concept won't be usable with the function, even though it would formally have been enough.

Concepts из <concepts> проверяют синтаксис и типажи, а не поведениеConcepts from <concepts> check syntax and traits, not behavior

Как и любой concept (см. concepts — синтаксис языка), std::equality_comparable лишь проверяет, что operator== определён и возвращает нечто, приводимое к bool — он не проверяет, что сравнение действительно отражает равенство значений. Тип с "сравнением" по указателю вместо содержимого формально удовлетворит concept.Like any concept (see concepts — language syntax), std::equality_comparable only checks that operator== is defined and returns something convertible to bool — it doesn't check that the comparison actually reflects value equality. A type that "compares" by pointer identity rather than content will formally satisfy the concept.