C++20 concepts are a powerful feature designed to enhance template programming by specifying constraints on template parameters, leading to clearer code and more informative error messages. Here’s a structured overview:
1. Concept Definition
Use the concept
keyword to define constraints on types:
#include <concepts>
template<typename T>
concept Incrementable = requires(T t) {
{ ++t } -> std::same_as<T&>;
};
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
template<typename T>
concept HasSize = requires(T t) {
{ t.size() } -> std::convertible_to<size_t>;
};
2. Applying Concepts
In Function Templates
Direct Syntax:
template<Incrementable T>
void func(T t) { ... }
requires
Clause:
template<typename T>
requires Addable<T>
T add(T a, T b) { return a + b; }
Abbreviated Syntax:
void print_size(HasSize auto t) {
std::cout << t.size() << '\n';
}
In Class Templates
template<Addable T>
class Container {
// T must satisfy Addable
};
3. Standard Concepts
The <concepts>
header provides predefined concepts:
- Type Categories:
std::integral
,std::floating_point
. - Comparisons:
std::equality_comparable
,std::totally_ordered
. - Object Lifetime:
std::movable
,std::copyable
. - Operations:
std::invocable
(for callable types).
4. Combining Concepts
Use logical operators to compose constraints:
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<typename T>
concept SizedContainer = HasSize<T> && requires(T t) {
typename T::value_type;
};
5. Overloading with Concepts
Enable function overloads based on constraints:
template<std::integral T>
void process(T t) { /* Handle integers */ }
template<std::floating_point T>
void process(T t) { /* Handle floats */ }
6. Requires Expressions
Check validity of operations, types, or expressions:
template<typename T>
concept Iterable = requires(T t) {
t.begin(); // Must have begin()
t.end(); // Must have end()
typename T::iterator; // Must define iterator type
};
7. Benefits
- Clarity: Explicitly state template requirements.
- Error Messages: Compilers generate clearer messages when constraints fail.
- Simplification: Reduces reliance on SFINAE and
enable_if
.
Example: Container Concept
template<typename C>
concept Container = requires(C c) {
c.begin();
c.end();
typename C::value_type;
};
template<Container C>
void print(const C& c) {
for (const auto& elem : c)
std::cout << elem << ' ';
}
8. Key Notes
- Subsumption: The compiler selects the most specific concept during overload resolution.
- Nesting: Concepts can include nested
requires
clauses for complex constraints. - Compatibility: Works seamlessly with
auto
, lambdas, and other modern C++ features.
Common Pitfalls
- Over-constraining: Adding unnecessary restrictions that limit usability.
- Under-constraining: Failing to capture all required operations, leading to runtime errors.
- Syntax Errors: Incorrect placement of
requires
clauses or expressions.
By leveraging concepts, developers can write more expressive, robust, and maintainable template code in C++. Practice defining and combining concepts to fully utilize this feature.