Concepts turn template errors from pages of noise into one line and document what a template actually needs.
A concept is a named compile-time predicate on types. std::integral<T> is true for integer types; std::totally_ordered<T> for types with all comparisons. The header <concepts> provides the core set (same_as, convertible_to, derived_from, invocable, regular...), and <iterator> and <ranges> add many more.
You can constrain a template in several equivalent ways:
template <std::integral T> T half(T x);template <typename T> requires std::integral<T> T half(T x);- Abbreviated:
auto half(std::integral auto x);
If a call doesn't satisfy the constraint, the error names the failed concept at the call site instead of failing somewhere inside the template body.
Define your own with a requires-expression, which lists the operations a type must support:
template <typename T>
concept Shape = requires(const T& s) {
{ s.area() } -> std::convertible_to<double>;
};Each requirement must compile for the concept to hold; the -> concept part also checks the result type.
Constraints take part in overload resolution. You can write one half for integers and one for floating-point types, and the right one is picked; when one constraint subsumes another (it includes it via &&), the more constrained overload wins. This replaces most uses of SFINAE and enable_if.
Concepts are checked structurally: a type satisfies Shape if it has the right members, without inheriting from anything. That's static duck typing with good errors.
Use the standard concepts when they fit, and design your own around meaningful abstractions ("a Shape", "a Sortable range") rather than single operations.
Example
#include <concepts>
#include <iostream>
#include <string>
template <typename T>
concept Shape = requires(const T& s) {
{ s.area() } -> std::convertible_to<double>; // must have area() giving ~double
{ s.name() } -> std::convertible_to<std::string>;
};
struct Square {
double side;
double area() const { return side * side; }
std::string name() const { return "square"; }
};
void describe(const Shape auto& s) { // abbreviated function template
std::cout << s.name() << ": " << s.area() << '\n';
}
template <typename T> requires std::integral<T>
T half(T x) { return x / 2; }
template <std::floating_point T> // chosen for float and double
T half(T x) { return x / 2.0; }
int main() {
describe(Square{3}); // square: 9
std::cout << half(7) << ' ' << half(7.0) << '\n'; // 3 3.5
// describe(42); // error: constraints not satisfied (Shape<int> is false)
}Remember
- Constrain templates with
template <Concept T>,requires, orConcept auto - Define concepts with requires-expressions listing the needed operations
- Constraints participate in overload resolution and replace most SFINAE
Watch out for
- Writing a concept for a single syntactic operation ("HasPlus") instead of a meaningful abstraction.