Последнее обновление:2026-10-12
Концепции Concepts в C++20¶
В C++20 появились concepts (концепции) — набор ограничений для шаблонных параметров (вычисляемых на этапе компиляции). Их можно применять к шаблонам классов и шаблонам функций для управления перегрузкой функций и специализацией. Среди преимуществ:
- Принудительные ограничения типов для шаблонных параметров
- Повышение читабельности кода (заменяет громоздкий SFINAE-код)
- Более понятные сообщения об ошибках
- Предотвращение непреднамеренного инстанцирования шаблонов за счёт ограничения допустимых типов
Возьмём простой пример сложения.
#include <iostream>
struct Foo {};
template <typename T>
T Add(T a, T b) {
return a + b;
}
int main() {
std::cout << Add(1, 2) << std::endl;
Foo f1, f2;
std::cout << Add(f1, f2) << std::endl;
return 0;
}
Для Foo сложение не поддерживается, и одновременно нельзя напрямую писать std::cout <<, поэтому при компиляции возникает куча ошибок, связанных с operator<< и operator+; но это не те сообщения об ошибках, которых мы ждём — нам хотелось бы, чтобы компилятор выдал максимально наглядную ошибку, а именно: можно ли складывать эту структуру.
add.cc: In function 'int main()':
add.cc:13:13: error: no match for 'operator<<' (operand types are 'std::ostream' {aka 'std::basic_ostream<char>'} and 'Foo')
13 | std::cout << Add(f1, f2) << std::endl;
| ~~~~~~~~~ ^~ ~~~~~~~~~~~
| | |
| | Foo
| std::ostream {aka std::basic_ostream<char>}
In file included from /usr/local/Cellar/gcc/13.2.0/include/c++/13/iostream:41,
from add.cc:1:
/usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream:110:7: note: candidate: 'std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(__ostream_type& (*)(__ostream_type&)) [with _CharT = char; _Traits = std::char_traits<char>; __ostream_type = std::basic_ostream<char>]'
110 | operator<<(__ostream_type& (*__pf)(__ostream_type&))
/usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream:801:5: note: template argument deduction/substitution failed:
/usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream: In substitution of 'template<class _Ostream, class _Tp> _Ostream&& std::operator<<(_Ostream&&, const _Tp&) [with _Ostream = std::basic_ostream<char>&; _Tp = Foo]':
add.cc:13:26: required from here
/usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream:801:5: error: no type named 'type' in 'struct std::enable_if<false, void>'
add.cc: In instantiation of 'T Add(T, T) [with T = Foo]':
add.cc:13:19: required from here
add.cc:7:12: error: no match for 'operator+' (operand types are 'Foo' and 'Foo')
7 | return a + b;
| ~~^~~
Когда же мы реализуем это с помощью concept:
template <typename T>
concept Addable = requires(T a, T b) { a + b; };
template <typename T>
requires Addable<T>
T Add(T a, T b) {
return a + b;
}
то получим именно ту ошибку компиляции, которая нас интересует:
add_concept.cc: In function 'int main()':
add_concept.cc:17:19: error: no matching function for call to 'Add(Foo&, Foo&)'
17 | std::cout << Add(f1, f2) << std::endl;
| ~~~^~~~~~~~
add_concept.cc:10:3: note: candidate: 'template<class T> requires Addable<T> T Add(T, T)'
10 | T Add(T a, T b) {
| ^~~
add_concept.cc:10:3: note: template argument deduction/substitution failed:
add_concept.cc:10:3: note: constraints not satisfied
add_concept.cc: In substitution of 'template<class T> requires Addable<T> T Add(T, T) [with T = Foo]':
add_concept.cc:17:19: required from here
add_concept.cc:6:9: required for the satisfaction of 'Addable<T>' [with T = Foo]
add_concept.cc:6:19: in requirements with 'T a', 'T b' [with T = Foo]
add_concept.cc:6:42: note: the required expression '(a + b)' is invalid
6 | concept Addable = requires(T a, T b) { a + b; };
| ~~^~~
Далее на этом примере глубоко изучим синтаксис concept.
1.Синтаксис concept¶
Синтаксис:
Сопоставим с реальным примером: Addable — это concept-name, а constraint-expression — это requires(T a, T b) { a + b; }.
Способ использования:
Этот concept можно ставить в несколько мест, а именно:
- на место typename
- после requires
- перед auto
1.1 Замена typename¶
Ограничивает шаблонный параметр, заменяя typename.
1.2 Ключевое слово requires¶
Мы используем ключевое слово requires в шаблоне функции. Оно позволяет проверить, является ли наш шаблонный T складываемым, и если шаблон может обрабатывать сложение, вернётся true.
requires можно поместить в шаблон, а также после функции, но нельзя после класса. При этом у него две формы записи:
- requires-условие
Например:
- requires-выражение
Например:
1.2.1 Помещаем в шаблон¶
Функция:
Класс:
1.2.2 Завершающая Requires-конструкция функции¶
Функция:
Для класса такая запись не поддерживается и вызовет ошибку: error: expected unqualified-id before 'requires' 28 | requires Addable
1.2.3 Базовый пример с requires¶
Возьмём типы из базы данных: в базе данных есть разные типы, которые мы разделим на null, binary, number и т. д. Мы хотим выполнять операцию печати для передаваемых типов — отсюда следующий пример:
#include <concepts>
class NumberType {};
class BaseBinaryType {};
class NullType {};
class FloatingPointType : public NumberType {};
class IntegerType : public NumberType {};
class BinaryType: public BaseBinaryType {};
template <typename T>
requires std::is_base_of_v<NumberType, T> || std::is_base_of_v<BaseBinaryType, T>
void PrintValue(T v) {}
int main() {
PrintValue(FloatingPointType{});
PrintValue(NullType{});
return 0;
}
В requires можно использовать ||; в примере выше появился NullType, который не удовлетворяет requires, поэтому компиляция выдаст:
concept_requires.cc:16:13: error: no matching function for call to 'PrintValue(NullType)'
16 | PrintValue(NullType{});
| ~~~~~~~~~~^~~~~~~~~~~~
concept_requires.cc:12:6: note: candidate: 'template<class T> requires (is_base_of_v<NumberType, T>) || (is_base_of_v<BaseBinaryType, T>) void PrintValue(T)'
12 | void PrintValue(T v) {}
| ^~~~~~~~~~
concept_requires.cc:12:6: note: template argument deduction/substitution failed:
concept_requires.cc:12:6: note: constraints not satisfied
concept_requires.cc: In substitution of 'template<class T> requires (is_base_of_v<NumberType, T>) || (is_base_of_v<BaseBinaryType, T>) void PrintValue(T) [with T = NullType]':
concept_requires.cc:16:13: required from here
concept_requires.cc:12:6: required by the constraints of 'template<class T> requires (is_base_of_v<NumberType, T>) || (is_base_of_v<BaseBinaryType, T>) void PrintValue(T)'
concept_requires.cc:11:45: note: no operand of the disjunction is satisfied
11 | requires std::is_base_of_v<NumberType, T> || std::is_base_of_v<BaseBinaryType, T>
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1.3 concept и auto¶
Когда появляется auto, мы можем использовать его вместе с concept, например:
Можно переписать так:
template <typename T>
concept Addable = requires(T a, T b) { a + b; };
auto add2(Addable auto x, Addable auto y) {
return x + y;
}
При компиляции получим:
concept_auto.cc:17:20: error: no matching function for call to 'add2(Foo&, Foo&)'
17 | std::cout << add2(f1, f2) << std::endl;
| ~~~~^~~~~~~~
concept_auto.cc:10:6: note: candidate: 'template<class auto:18, class auto:19> requires (Addable<auto:18>) && (Addable<auto:19>) auto add2(auto:18, auto:19)'
10 | auto add2(Addable auto x, Addable auto y) {
| ^~~~
concept_auto.cc:10:6: note: template argument deduction/substitution failed:
concept_auto.cc:10:6: note: constraints not satisfied
concept_auto.cc: In substitution of 'template<class auto:18, class auto:19> requires (Addable<auto:18>) && (Addable<auto:19>) auto add2(auto:18, auto:19) [with auto:18 = Foo; auto:19 = Foo]':
concept_auto.cc:17:20: required from here
concept_auto.cc:8:9: required for the satisfaction of 'Addable<auto:18>' [with auto:18 = Foo]
concept_auto.cc:8:19: in requirements with 'T a', 'T b' [with T = Foo]
concept_auto.cc:8:42: note: the required expression '(a + b)' is invalid
8 | concept Addable = requires(T a, T b) { a + b; };
2.Поддержка компиляторов¶
Требуются GCC(10.0+), Clang(10.0+), опция компиляции: -std=c++20/-std=c++2a
3.Итоги¶
С появлением concept в C++20 нам больше не нужны механизмы enable_if/SFINAE и перегрузки функций для проверок шаблонных ограничений — concept поможет справиться с этим. Он предоставляет более ясный и мощный механизм ограничений шаблонных параметров, делая шаблонный код проще в написании, понимании и сопровождении. Выполняя проверку типов на этапе компиляции, он способствует повышению надёжности и читабельности кода.
Похожие темы¶
- Введение в корутины C++20 | C++那些事
- Практика с channel в C++20 | C++那些事
- constexpr и оператор «запятая» в C++20 | C++那些事
- Порядок байтов endian в C++20 | C++那些事
- Лямбда-выражения в C++20 (часть 1) | C++那些事
- Модули Modules в C++20 | C++那些事
- Библиотека Ranges в C++20 | C++那些事
- Дизайн и подводные камни std::span в C++20 | C++那些事
- Обзор новых возможностей C++20 | C++那些事
- [[likely]] и [[unlikely]] в C++20 | C++那些事