Concepts (C++20)
Named type constraints for clearer template interfaces
The Template Problem
Traditional templates produce cryptic error messages when type requirements aren't met.
// C++17: Template with SFINAE
template<typename T>
auto square(T value) -> std::enable_if_t<std::is_arithmetic_v<T>, T> {
return value * value;
}
// Calling with wrong type:
square(std::string{"hello"});
// Error: no matching function... enable_if_t failed...
// User must understand SFINAE to interpret errorC++20: Concepts
Concepts are named sets of type requirements that produce clear error messages.
// C++20: Define a concept
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
// Use concept to constrain template
template<Arithmetic T>
T square(T value) {
return value * value;
}
// Error is now clear:
// "constraint 'Arithmetic' was not satisfied"
// "'std::is_arithmetic_v<std::string>' evaluated to false"Defining Concepts
1. Basic Syntax
// Simple concept using type trait
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
// Concept with expression requirements
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
// Concept with multiple requirements
template<typename T>
concept Container = requires(T t) {
typename T::value_type;
{ t.begin() } -> std::same_as<typename T::iterator>;
{ t.end() } -> std::same_as<typename T::iterator>;
{ t.size() } -> std::convertible_to<std::size_t>;
};2. Compound Concepts
// Combine concepts
template<typename T>
concept Sortable = Container<T> &&
std::random_access_iterator<typename T::iterator> &&
std::totally_ordered<typename T::value_type>;
// Refine existing concepts
template<typename T>
concept SignedIntegral = std::integral<T> && std::is_signed_v<T>;
template<typename T>
concept UnsignedIntegral = std::integral<T> && std::is_unsigned_v<T>;Applying Concepts
1. Template Parameters
// Constrain template parameter
template<std::floating_point T>
T compute_area(T radius) {
return 3.14159 * radius * radius;
}
// Multiple concepts
template<std::integral Index, typename Value>
Value get(const std::vector<Value>& v, Index i) {
return v[i];
}2. Abbreviated Function Templates
// Auto parameter with concept (C++20 abbreviated syntax)
void process(std::integral auto value) {
// Only accepts integral types
}
// Multiple parameters
double divide(std::floating_point auto a, std::floating_point auto b) {
return a / b;
}3. Trailing Requires
// Requires clause
template<typename T>
requires Addable<T> && std::copyable<T>
T sum(const std::vector<T>& values) {
T result{};
for (const auto& v : values) {
result = result + v;
}
return result;
}Standard Library Concepts
| Category | Concepts |
|---|---|
| Core Language | same_as, derived_from, convertible_to |
| Arithmetic | integral, signed_integral, unsigned_integral, floating_point |
| Comparison | equality_comparable, totally_ordered |
| Object | copyable, movable, default_initializable |
| Callable | invocable, regular_invocable, predicate |
| Iterator | input_iterator, forward_iterator, random_access_iterator |
| Range | range, sized_range, view, input_range |
Real-World Example: Safe Math
#include <concepts>
#include <stdexcept>
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
// Checked arithmetic - prevents overflow
template<Numeric T>
T safe_add(T a, T b) {
if constexpr (std::integral<T>) {
if (b > 0 && a > std::numeric_limits<T>::max() - b) {
throw std::overflow_error("Integer overflow");
}
if (b < 0 && a < std::numeric_limits<T>::min() - b) {
throw std::overflow_error("Integer underflow");
}
}
return a + b;
}
// Vector operations
template<Numeric T>
class Vector2D {
T x_, y_;
public:
Vector2D(T x, T y) : x_(x), y_(y) {}
T magnitude() const
requires std::floating_point<T>
{
return std::sqrt(x_ * x_ + y_ * y_);
}
auto dot(const Vector2D& other) const {
return safe_add(safe_add(T{}, x_ * other.x_), y_ * other.y_);
}
};Concept Overloading
Concepts enable function overloading based on type capabilities:
// Fast path for random access iterators
template<std::random_access_iterator Iter>
void advance(Iter& it, int n) {
it += n; // O(1)
}
// Slower path for forward iterators
template<std::forward_iterator Iter>
void advance(Iter& it, int n) {
for (int i = 0; i < n; ++i) {
++it; // O(n)
}
}
// Most general path
template<typename Iter>
void advance(Iter& it, int n) {
// Fallback implementation
}Best Practices
- ✓Name concepts after the operations they enable (Addable, Sortable, Drawable)
- ✓Use standard concepts when they exist (don't reinvent integral)
- ✓Prefer abbreviated syntax for simple cases
- ✗Don't over-constrain (e.g., requiring random_access when forward is enough)
- ✗Avoid concepts that combine unrelated requirements