Привет, Хабр!
Каждый C++‑разработчик хоть раз в жизни получал ошибку компиляции шаблона на 200 строк. Вы передали std::string в функцию, которая ожидала тип с operator<. Компилятор не сказал «у string нет operator< с этим типом». Он выдал стену текста про какой‑то detail::move_if_noexcept_cond внутри реализации std::sort, о которой вы не просили.
Concepts в C++ все это решили. Concept — именованный набор требований к типу, проверяемый на этапе компиляции. Но concepts — это не только про ошибки, он про читаемость, перегрузку, документацию кода. Разберём.
Проблема: что такое T?
template<typename T> T max(T a, T b) { return a > b ? a : b; }
Что такое T? Любой тип? Нет, только тип с operator>. Но это неявное требование. Компилятор узнает о нём только при инстанцировании шаблона. Пользователь шаблона узнает о нём только из ошибки компиляции (или из документации, если повезёт).
С concepts:
template<typename T> concept Comparable = requires(T a, T b) { { a > b } -> std::convertible_to<bool>; { a < b } -> std::convertible_to<bool>; { a == b } -> std::convertible_to<bool>; }; template<Comparable T> T max(T a, T b) { return a > b ? a : b; }
Теперь требование явное. Comparable — concept, который говорит: «тип должен поддерживать >, < и == с результатом, конвертируемым в bool». Если передать тип без operator> ошибка будет на уровне concept, а не где‑то в кишках реализации.
Синтаксис: четыре способа использовать concept
// 1. Constrained template parameter (самый чистый) template<Comparable T> T max(T a, T b); // 2. requires-clause после template template<typename T> requires Comparable<T> T max(T a, T b); // 3. Trailing requires (после параметров) template<typename T> T max(T a, T b) requires Comparable<T>; // 4. Abbreviated function template (самый краткий) auto max(Comparable auto a, Comparable auto b) { return a > b ? a : b; }
Все четыре эквивалентны. Первый и четвёртый на мой взгляд самые читаемые. Четвёртый хорош для простых функций, даже template<> писать не нужно.
Если хотите быстро понять, насколько уверенно ориентируетесь в современном C++, начните с короткого теста. ➦
[Пройти тест]
requires‑выражения: описываем требования
requires — ключевое слово для описания набора требований к типу:
template<typename T> concept Container = requires(T c) { // Простое требование: выражение должно быть валидным c.begin(); c.end(); c.size(); // Требование вложенного типа typename T::value_type; typename T::iterator; // Составное требование: выражение валидно И возвращает конкретный тип { c.size() } -> std::convertible_to<std::size_t>; { *c.begin() } -> std::same_as<typename T::value_type&>; // Вложенное требование: другой constraint requires std::default_initializable<T>; };
Четыре вида требований внутри requires:
Простое — выражение компилируется:
c.begin().Типовое — тип существует:
typename T::value_type.Составное — выражение компилируется и возвращает нужный тип:
{ c.size() } -> std::convertible_to<std::size_t>.Вложенное — другое ограничение выполняется:
requires std::default_initializable<T>.
Стандартные concepts: не изобретайте велосипед
Заголовок <concepts> содержит набор готовых concepts:
#include <concepts> // Базовые std::same_as<T, U> // T и U — один тип std::derived_from<T, Base> // T наследует Base std::convertible_to<T, U> // T конвертируется в U // Арифметика std::integral<T> // целочисленный тип std::floating_point<T> // тип с плавающей точкой std::signed_integral<T> // знаковый целый // Сравнение std::equality_comparable<T> // поддерживает == и != std::totally_ordered<T> // поддерживает <, >, <=, >=, ==, != // Объекты std::movable<T> // move-конструктор + move-присваивание std::copyable<T> // copy + move std::regular<T> // copyable + default_initializable + equality_comparable // Вызываемые std::invocable<F, Args...> // F можно вызвать с Args std::predicate<F, Args...> // F(Args...) возвращает bool
Используйте стандартные concepts вместо написания своих, где возможно:
// Плохо: свой concept для того, что уже есть template<typename T> concept MyIntegral = std::is_integral_v<T>; // Хорошо: стандартный template<std::integral T> T absolute(T value) { return value < 0 ? -value : value; }
Композиция: concepts из concepts
Concepts комбинируются через && и ||:
template<typename T> concept Numeric = std::integral<T> || std::floating_point<T>; template<typename T> concept Printable = requires(std::ostream& os, T value) { { os << value } -> std::same_as<std::ostream&>; }; template<typename T> concept PrintableNumeric = Numeric<T> && Printable<T>; void display(PrintableNumeric auto value) { std::cout << "Value: " << value << "\n"; }
Можно уточнять (refine) — более специфичный concept включает менее специфичный:
template<typename T> concept SignedNumeric = Numeric<T> && std::is_signed_v<T>;
Перегрузка через concepts
С Concepts дают можно делать лаконичную перегрузку:
template<std::integral T> std::string serialize(T value) { return std::to_string(value); } template<std::floating_point T> std::string serialize(T value) { std::ostringstream oss; oss << std::fixed << std::setprecision(2) << value; return oss.str(); } template<typename T> requires requires(T t) { { t.toString() } -> std::convertible_to<std::string>; } std::string serialize(T value) { return value.toString(); } serialize(42); // integral → "42" serialize(3.14159); // floating_point → "3.14" serialize(myObj); // has toString() → myObj.toString()
Компилятор выбирает наиболее специфичный concept. Если тип удовлетворяет нескольким — выбирается тот, чей concept «сильнее. std::signed_integral сильнее std::integral, потому что включает его как часть определения.»
Concept для своей библиотеки
Допустим, пишем библиотеку сериализации. Определяем concept для типов, которые мы умеем сериализовать:
template<typename T> concept Serializable = requires(T value, std::ostream& out, std::istream& in) { { value.serialize(out) } -> std::same_as<void>; { T::deserialize(in) } -> std::same_as<T>; typename T::SerializationTag; // Маркерный тип для версионирования }; template<Serializable T> void saveToFile(const T& obj, const std::string& path) { std::ofstream file(path, std::ios::binary); obj.serialize(file); } template<Serializable T> T loadFromFile(const std::string& path) { std::ifstream file(path, std::ios::binary); return T::deserialize(file); } // Пользовательский тип struct Config { using SerializationTag = struct V1; std::string name; int version; void serialize(std::ostream& out) const { out.write(name.c_str(), name.size()); out.write(reinterpret_cast<const char*>(&version), sizeof(version)); } static Config deserialize(std::istream& in) { // ... } }; Config cfg{"app", 1}; saveToFile(cfg, "config.bin"); // OK: Config удовлетворяет Serializable // saveToFile(42, "x.bin"); // Ошибка: int не удовлетворяет Serializable
Concept — это документация и контракт одновременно. Читатель видит
Serializableи понимает требования. Компилятор проверяет их при каждом использовании.
constexpr + concepts: проверки на этапе компиляции
Concepts работают в constexpr if и static_assert:
template<typename T> void process(T value) { if constexpr (std::integral<T>) { // Целочисленная логика auto result = value << 2; } else if constexpr (std::floating_point<T>) { // Дробная логика auto result = std::sqrt(value); } else { static_assert(Printable<T>, "T must be at least Printable"); std::cout << value; } }
constexpr if с concepts — замена длинных цепочек std::enable_if и SFINAE‑трюков. Читается как обычный if, но компилируется только нужная ветка.
Ошибки: до и после
До concepts (SFINAE):
error: no matching function for call to 'sort(std::vector<MyType>&)' note: candidate template ignored: substitution failure [with T = MyType]: no member named 'operator<' in 'MyType' ... (ещё 50 строк внутренностей STL)
После concepts:
error: no matching function for call to 'sort' note: constraints not satisfied note: because 'MyType' does not satisfy 'sortable' note: because 'a < b' would be invalid: invalid operands to binary expression
Concepts — фундаментальное улучшение вашего кода: читаемые требования к типам, понятные ошибки, чистая перегрузка, самодокументирующийся код.

Тем, кому уже мало просто рабочего кода и важно лучше контролировать сложность, архитектуру и поведение программы, стоит присмотреться к курсу «C++‑разработчик. Продвинутый уровень».
До 31 марта действует дополнительная скидка 10% по промокоду birthday — и она суммируется с другими скидками. Так что курс можно взять на более выгодных условиях уже сейчас. ➦
[Забрать курс со скидкой]
А если хотите глубже погрузиться в практику современного C++ и посмотреть, как эти знания применяются в реальных задачах разработки, можно начать с открытых уроков:
30 марта, 20:00 — "Основы 2D и 3D‑графики на C++ в Linux"
Разберётесь, как устроена графика в Linux‑приложениях на C++. ☛[Записаться]13 апреля, 20:00 — "Паттерны проектирования в C++"
Поймёте, как делать код более управляемым и расширяемым. ☛[Записаться]23 апреля, 20:00 — "Многопоточность в C++"
Узнаете, как писать быстрые и безопасные многопоточные приложения. ☛[Записаться]
