C++20 Concepts & Constraints
Definujte přesné požadavky na argumenty šablony pomocí konceptů. Napište čistší generický kód s požadovanými klauzulemi, zkrácenými šablonami funkcí a koncepty standardních knihoven.
The Problem Concepts Solve
Before concepts, template error messages were cryptic — usingstd::sorton a type withoutoperator<could produce pages of error text deep inside the standard library implementation. Concepts solve this by letting you state requirements up front on template parameters.
A concept is a named boolean predicate evaluated at compile time. It checks whether a type satisfies a set of syntactic and semantic requirements. When a concept is not satisfied, the compiler produces a clear, concise error message pointing to the unsatisfied requirement — not a wall of template instantiation backtraces.
Concepts were introduced in C++20 and represent the most significant improvement to generic programming since templates themselves.
Definování pojmů
Koncept je definován pomocí klíčového slovaconceptnásledovaného výrazem vyžaduje výraz, který určuje, jaké operace musí typ podporovat. Výraz vyžaduje může kontrolovat platné výrazy, návratové typy, vnořené typy a další.
#include <concepts>
#include <iostream>
#include <string>
// Define a concept: T must support +, ==, and copy construction
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // a+b must return something convertible to T
{ a == b } -> std::convertible_to<bool>; // must be comparable
requires std::copy_constructible<T>; // nested requirement
};
// Concept for types that have .size() and .empty()
template <typename T>
concept Sizable = requires(const T& t) {
{ t.size() } -> std::convertible_to<std::size_t>;
{ t.empty() } -> std::convertible_to<bool>;
};
// Use the concept to constrain a template
template <Addable T>
T sum_three(T a, T b, T c) {
return a + b + c;
}
template <Sizable Container>
void report_size(const Container& c) {
std::cout << "Size: " << c.size()
<< (c.empty() ? " (empty)" : "") << '\n';
}
int main() {
std::cout << sum_three(1, 2, 3) << '\n'; // 6
std::cout << sum_three(1.5, 2.5, 3.0) << '\n'; // 7.0
std::string s = "hello";
report_size(s); // Size: 5
std::vector<int> v{1, 2, 3};
report_size(v); // Size: 3
}Vyžaduje klauzule a zkrácenou syntaxi
There are multiple ways to apply a concept to a template. The requires clause goes after the template parameter list or after the function signature. The abbreviated function template syntax usesConcept autoas a parameter type, which is the most concise form.
#include <concepts>
#include <iostream>
// Method 1: Concept as template parameter constraint
template <std::integral T>
T gcd(T a, T b) {
while (b != 0) {
T temp = b;
b = a % b;
a = temp;
}
return a;
}
// Method 2: requires clause after template parameters
template <typename T>
requires std::integral<T>
T lcm(T a, T b) {
return (a / gcd(a, b)) * b;
}
// Method 3: Trailing requires clause
template <typename T>
T absolute(T value) requires std::signed_integral<T> {
return value < 0 ? -value : value;
}
// Method 4: Abbreviated function template (Concept auto)
void print_integral(std::integral auto value) {
std::cout << "Integral: " << value << '\n';
}
// Concept auto works for return types too (C++20)
std::integral auto compute() {
return 42; // must return an integral type
}
int main() {
std::cout << gcd(12, 8) << '\n'; // 4
std::cout << lcm(12, 8) << '\n'; // 24
std::cout << absolute(-42) << '\n'; // 42
print_integral(100); // Integral: 100
// print_integral(3.14); // ERROR: constraint not satisfied
}Složení konceptu a standardní koncepty
Concepts compose naturally with && (conjunction) and || (disjunction). The standard library provides a rich set of concepts in :
Core language concepts:
-std::same_as— T and U are the same type
-std::derived_from— Derived is derived from Base
-std::convertible_to— From is implicitly convertible to To
-std::integral,std::floating_point,std::signed_integral
Comparison concepts:
-std::equality_comparable— supports==and!=
-std::totally_ordered— supports<,>,<=,>=,==,!=
Object concepts:
-std::movable,std::copyable,std::semiregular,std::regular
Callable concepts:
-std::invocable— F can be called with Args
-std::predicate— F is a predicate returning bool
Iterator concepts ():
-std::input_iterator,std::forward_iterator,std::random_access_iterator,std::contiguous_iterator
Range concepts ():
-std::ranges::range,std::ranges::sized_range,std::ranges::random_access_range
Rozlišení konceptu subsumpce a přetížení
When multiple constrained overloads match, the compiler uses concept subsumption to pick the most specific one. Concept A subsumes concept B if A's constraints logically imply B's constraints. This allows "refinement" — a more specific concept wins over a more general one.
#include <concepts>
#include <iostream>
// Concept hierarchy: sorted_range is "more specific" than range
template <typename T>
concept Printable = requires(std::ostream& os, const T& t) {
{ os << t };
};
template <typename T>
concept PrintableNumber = Printable<T> && std::integral<T>;
// Less constrained overload
template <Printable T>
void display(const T& value) {
std::cout << "[generic] " << value << '\n';
}
// More constrained overload — subsumes Printable
template <PrintableNumber T>
void display(const T& value) {
std::cout << "[number] " << value << '\n';
}
int main() {
display(42); // [number] 42 — PrintableNumber is more specific
display(std::string("hi")); // [generic] hi — only Printable matches
display(3.14); // [generic] 3.14 — not integral
}Vyžaduje vyžaduje vzor
Poněkud neobvyklé requires requires se objeví, když použijete výraz ad-hoc vyžaduje přímo v klauzuli required bez definování pojmenovaného konceptu. Prvnírequireszavádí omezující klauzuli; druhýrequireszačíná požadovaný výraz.
#include <iostream>
#include <string>
// Ad-hoc constraint without naming a concept
template <typename T>
requires requires(T a, T b) {
{ a + b } -> std::same_as<T>;
}
T add(T a, T b) {
return a + b;
}
// Equivalent named concept (preferred for readability)
template <typename T>
concept AddableToSelf = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template <AddableToSelf T>
T add_named(T a, T b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << '\n'; // 3
std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab
std::cout << add_named(10, 20) << '\n'; // 30
}Při navrhování konceptů:
- Pojmenujte pojmy podle toho, co JE typ, nikoli podle toho, co funkce potřebuje – použijteSortable, nikoliHasLessThan. Pojmy by měly zachycovat sémantické kategorie.
- Upřednostňujte standardní koncepty knihoven – než napíšete vlastní, zkontrolujte, zda,nebojiž obsahuje to, co potřebujete.
- Udržujte koncepty malé a složitelné – kombinujte malé koncepty s&&místo vytváření monolitických požadavků.
- Používejte pojmenované koncepty přes ad-hocrequires requires – pojmenovaná forma je čitelnější a znovu použitelná.
- Sémantické požadavky dokumentu v komentářích — koncepty kontrolují syntaxi, nikoli sémantiku. Pokud požadujete, aby byloperator+komutativní, řekněte to v komentáři.
- Koncepty se nazývají booleovské predikáty, které omezují parametry šablony a poskytují jasné chybové zprávy
- Výraz
requireskontroluje platné výrazy, návratové typy a vnořené požadavky - Čtyři způsoby, jak aplikovat koncepty: omezení na parametr šablony, klauzule required, koncové požadavky nebo
Concept auto - Koncepty tvoří
&&a||; subsumpce vybere nejvíce omezené přetížení - Standardní knihovna poskytuje koncepty v
,a requires requiresje ad-hoc inline omezení – kvůli čitelnosti preferujte pojmenované koncepty
Kvíz — Otestujte své znalosti
(20 XP)1. Co znamená`void print(std::integral auto value)`?
2. Pokud je koncept A definován jako`concept A = B<T> && C<T>`, co se stane, když se shoduje přetížení s omezením B a A?
3. Why does`requires requires(T a) { a.size(); }`have the keyword 'requires' twice?