Concepts in C++20 provide a clean way to express what types your templates expect. This makes template code more readable and helps catch type errors at compile time, rather than during debugging.
#include <concepts>
#include <iostream>
template<typename T>
concept Printable = requires(T t) {
std::cout << t;
};
template<Printable T>
void print(const T& value) {
std::cout << value << "\n";
}
In this example, Printable
is a concept that checks if a type can be used with std::cout
. The print function can only be used with types that satisfy this condition.
Why use concepts:
- To define clear constraints on templates
- To make error messages easier to understand
- To avoid complicated workarounds with enable_if
When to use:
- When writing generic functions or classes
- When you want to enforce that types meet specific requirements
- When you want to document assumptions about types directly in the code
Combining multiple requirements
We can use logical operators inside concepts:
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T>
T add(T a, T b) {
return a + b;
}
Here, add works for both int and double, but not for std::string
or std::vector
.
We can also write our own concepts
We are not limited to standard ones like std::integral
. We can define our own for interfaces we care about:
template<typename T>
concept has_size = requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
};
This allows us to constrain functions to work only with containers or classes that define a .size()
method returning a number.
The common pitfall is that concepts DO NOT check semantics
They check whether code compiles, not whether it does what you intended.
template<typename T>
concept comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};
This concept will accept a type that defines operator<
, but it does not check whether that operator gives meaningful or consistent results.
Summary
Concepts help express intent, restrict misuse, and simplify template error messages. I use them when:
- I write generic code that should only work for certain kinds of types
- I want to prevent misuse of APIs
- I care about clearer compile-time diagnostics
But be aware of over-constraining or assuming concepts validate behavior, not just type compatibility.