Concepts: Constraining Templates
C++20 concepts provide a way to specify requirements on template parameters, replacing complex SFINAE with readable constraints.
Basic Concept Syntax
#include <concepts>
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;
template<Numeric T>
T add(T a, T b) {
return a + b;
}
add(5, 10); // OK: int is Numeric
add(3.14, 2.71); // OK: double is Numeric
// add("hello", "world"); // Error: const char* is not Numeric
Standard Library Concepts
#include <concepts>
// Fundamental concepts
std::integral<int>; // Integer types
std::floating_point<double>; // Floating-point types
std::signed_integral<int>; // Signed integers
std::unsigned_integral<size_t>; // Unsigned integers
// Comparison concepts
std::equality_comparable<std::string>;
std::totally_ordered<int>; // Has <, >, <=, >=, ==, !=
// Object concepts
std::movable<std::unique_ptr<int>>;
std::copyable<std::string>;
std::semiregular<int>; // Default constructible and copyable
std::regular<int>; // Semiregular and equality comparable
Defining Custom Concepts
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // Must support +
};
template<typename T>
concept Container = requires(T t) {
typename T::value_type; // Must have value_type
{ t.begin() }; // Must have begin()
{ t.end() }; // Must have end()
{ t.size() } -> std::convertible_to<size_t>; // size() returns size_t-like
};
template<Container C>
void process(const C& container) {
for (const auto& elem : container) {
// ...
}
}
requires Clauses
Trailing requires
template<typename T>
T max(T a, T b) requires std::totally_ordered<T> {
return (a > b) ? a : b;
}
Constrained auto
std::integral auto square(std::integral auto x) {
return x * x;
}
square(5); // OK
// square(3.14); // Error: double is not integral
Combining Concepts
template<typename T>
concept SignedIntegral = std::integral<T> && std::signed_integral<T>;
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<typename T>
concept Range = requires(T t) {
t.begin();
t.end();
};
requires Expression
template<typename T>
concept Drawable = requires(T t) {
{ t.draw() } -> std::same_as<void>; // Must have void draw()
{ t.position() } -> std::convertible_to<std::pair<int, int>>;
t.visible; // Must have visible member
};
template<typename T>
concept Arithmetic = requires(T a, T b) {
a + b;
a - b;
a * b;
a / b;
};
Nested Requirements
template<typename T>
concept Complex = requires(T t) {
requires std::regular<T>; // Must be regular
requires sizeof(T) >= 8; // Size requirement
{ t.real() } -> std::floating_point;
{ t.imag() } -> std::floating_point;
};
Subsumption
More constrained concept is preferred:
template<typename T>
concept Integral = std::is_integral_v<T>;
template<typename T>
concept SignedIntegral = Integral<T> && std::is_signed_v<T>;
void process(Integral auto x) {
std::cout << "Integral\\n";
}
void process(SignedIntegral auto x) { // More constrained
std::cout << "Signed integral\\n";
}
process(5); // Calls SignedIntegral version
process(5u); // Calls Integral version
Concept-Based Overloading
void print(std::integral auto x) {
std::cout << "Integer: " << x << '\\n';
}
void print(std::floating_point auto x) {
std::cout << "Float: " << x << '\\n';
}
print(42); // Calls integral version
print(3.14); // Calls floating_point version
Debugging Concept Failures
template<typename T>
concept Sortable = requires(T t) {
{ t.begin() } -> std::random_access_iterator;
{ t.end() } -> std::random_access_iterator;
};
std::list<int> lst;
// std::sort(lst.begin(), lst.end());
// Clear error: list iterators are not random_access_iterator
Abbreviated Function Templates
// Before concepts
template<typename T>
void func(T x);
// With concepts (C++20)
void func(auto x); // Unconstrained
void func(std::integral auto x); // Constrained
// Multiple parameters
void func2(std::integral auto x, std::floating_point auto y);
Practical Example: Generic Algorithm
template<typename Range, typename Pred>
requires std::ranges::range<Range> &&
std::predicate<Pred, std::ranges::range_value_t<Range>>
auto filter(Range&& r, Pred p) {
std::vector<std::ranges::range_value_t<Range>> result;
for (auto&& elem : r) {
if (p(elem)) {
result.push_back(elem);
}
}
return result;
}
Conceptual Check
What advantage do concepts have over SFINAE?
Runtime Environment
Interactive Lab
1#include <iostream>
2#include <concepts>
3#include <vector>
4
5template<typename T>
6concept Numeric = std::integral<T> || std::floating_point<T>;
7
8template<Numeric T>
9T multiply(T a, T b) {
10 return a * b;
11}
12
13// Constrained auto
14auto add(std::integral auto a, std::integral auto b) {
15 return a + b;
16}
17
18int main() {
19 std::cout << multiply(5, 10) << '\n';
20 std::cout << multiply(3.14, 2.0) << '\n';
21 std::cout << add(100, 200) << '\n';
22
23 // Compile-time check
24 static_assert(Numeric<int>);
25 static_assert(Numeric<double>);
26 static_assert(!Numeric<std::string>);
27
28 std::cout << "All concept checks passed\n";
29
30 return 0;
31}
System Console
Waiting for signal...