C++20 Concepte și constrângeri
Definiți cerințe precise privind argumentele șablon folosind concepte. Scrieți un cod generic mai curat, cu clauze requires, șabloane de funcții abreviate și concepte standard de bibliotecă.
Rezolvarea conceptelor problemei
Înainte de concepte, mesajele de eroare ale șablonului erau criptice - folosireastd::sortpe un tip fărăoperator<putea produce pagini de text de eroare adânc în implementarea bibliotecii standard. Conceptele rezolvă acest lucru, permițându-vă să precizați cerințele în față cu privire la parametrii șablonului.
Un concept este un predicat boolean numit evaluat în timpul compilării. Verifică dacă un tip îndeplinește un set de cerințe sintactice și semantice. Când un concept nu este satisfăcut, compilatorul produce un mesaj de eroare clar și concis care indică cerința nesatisfăcută – nu un zid de instanțiere de șablon.
Conceptele au fost introduse în C++20 și reprezintă cea mai semnificativă îmbunătățire a programării generice de la șabloanele în sine.
Concepte definitorii
Un concept este definit cu cuvântul cheieconcepturmat de o expresie necesită care specifică ce operații trebuie să suporte un tip. Expresia requires poate verifica expresii valide, tipuri returnate, tipuri imbricate și multe altele.
#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
}Necesită clauze și sintaxă abreviată
Există mai multe moduri de a aplica un concept la un șablon. Clauza necesită merge după lista de parametri șablon sau după semnătura funcției. Sintaxa șablonului de funcție abreviată foloseșteConcept autoca tip de parametru, care este cea mai concisă 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
}Compoziția conceptului și conceptele standard
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
Subsumarea conceptului și rezoluția supraîncărcării
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
}Modelul necesită necesități
The somewhat unusual requires requires appears when you use an ad-hoc requires expression directly in a requires clause without defining a named concept. The firstrequiresintroduces the constraint clause; the secondrequiresbegins the requires expression.
#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
}When designing concepts:
- Name concepts after what the type IS, not what the function needs — useSortablenotHasLessThan. Concepts should capture semantic categories.
- Prefer standard library concepts — before writing your own, check if,, oralready has what you need.
- Keep concepts small and composable — combine small concepts with&&rather than creating monolithic requirements.
- Use named concepts over ad-hocrequires requires — the named form is more readable and reusable.
- Document semantic requirements in comments — concepts check syntax, not semantics. If you requireoperator+to be commutative, say so in a comment.
- Conceptele sunt numite predicate booleene care constrâng parametrii șablonului, oferind mesaje de eroare clare
- Expresia
requiresverifică expresii valide, tipuri de returnare și cerințe imbricate - Patru moduri de a aplica concepte: constrângere asupra parametrului șablonului, clauză necesită, necesită final sau
Concept auto - Conceptele compun cu
&&și||; subsumarea selectează supraîncărcarea cea mai restrânsă - Biblioteca standard oferă concepte în
,și requires requireseste o constrângere ad-hoc în linie - preferați conceptele numite pentru lizibilitate
Test — Verifică-ți cunoștințele
(20 XP)1. Ce înseamnă`void print(std::integral auto value)`?
2. If concept A is defined as`concept A = B<T> && C<T>`, what happens when both a B-constrained and an A-constrained overload match?
3. Why does`requires requires(T a) { a.size(); }`have the keyword 'requires' twice?