Concepts and Requires — C++20 Constraints, std::integral, std::ranges::input_range, Template Constraints
In this tutorial, you will learn about Concepts and Requires. We cover key concepts, practical examples, and best practices to help you master this topic.
C++20 concepts introduce named constraints on template parameters, enabling readable type requirements with clear error messages and simpler generic code compared to SFINAE-based enable_if patterns.
What You'll Learn
You will define your own concepts with the concept keyword and requires expressions, use standard library concepts like std::integral, std::floating_point, and std::ranges::input_range, overload functions based on concept satisfaction, combine concepts with logical operators, and replace SFINAE with concepts for cleaner template code.
Why It Matters
SFINAE and enable_if work but produce cryptic error messages dozens of lines long. Concepts solve this with constraints that the compiler checks and reports by name. When you see template <std::integral T>, the intent is obvious. Concepts also improve overload resolution, code completion in IDEs, and make C++ generic programming accessible to developers who found template Metaprogramming intimidating.
Learning Path
graph LR
A["47: constexpr & consteval"] --> B["48: Concepts & Requires"]
B --> C["49: Type Traits & Metaprogramming"]
C --> D["50: Lambda Expressions"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Basic Concepts
A concept is a named boolean predicate evaluated at compile time on template arguments.
#include <iostream>
#include <concepts>
// Define a concept
template <typename T>
concept Integral = std::is_integral_v<T>;
// Use the concept to constrain a template
template <Integral T>
T half(T value) {
return value / 2;
}
// Equivalent syntax with 'requires'
template <typename T>
requires Integral<T>
T doubleIt(T value) {
return value * 2;
}
int main() {
std::cout << half(42) << "\n"; // 21 (int satisfies Integral)
std::cout << doubleIt(7) << "\n"; // 14
// half(3.14); // Error: double does not satisfy Integral
// Error message: constraints not satisfied
}
The compiler error when calling half(3.14) will explicitly say "constraints not satisfied" and show the concept Integral, making it immediately clear why the call failed.
Standard Library Concepts
C++20 provides over 50 standard concepts in <concepts> and <ranges>.
#include <iostream>
#include <concepts>
#include <string>
// std::integral, std::floating_point, std::signed_integral, std::unsigned_integral
// std::same_as, std::derived_from, std::convertible_to
// std::regular, std::semiregular, std::equality_comparable
// std::totally_ordered, std::movable, std::copyable
// std::invocable, std::predicate, std::relation
template <std::integral T>
T addOne(T value) {
return value + 1;
}
template <std::floating_point T>
T addOne(T value) {
return value + 1.0;
}
// Multiple constraints
template <typename T>
requires std::integral<T> || std::floating_point<T>
auto addOneGeneric(T value) {
return value + 1;
}
// Negated concept
template <typename T>
requires !std::integral<T> && !std::floating_point<T>
auto addOneGeneric(T value) {
return value + 1; // May not compile for non-arithmetic types
}
int main() {
std::cout << addOne(5) << "\n"; // 6 (integral)
std::cout << addOne(3.14) << "\n"; // 4.14 (floating_point)
// addOne(std::string("hello")); // Error: no matching overload
}
Writing Custom Concepts
Use requires expressions to specify what a type must support.
#include <iostream>
#include <vector>
#include <list>
#include <forward_list>
// Concept: type has begin() and end()
template <typename T>
concept Range = requires(T& t) {
std::begin(t);
std::end(t);
};
// Concept: type has size()
template <typename T>
concept Sized = requires(const T& t) {
{ t.size() } -> std::convertible_to<size_t>;
};
// Combined concept
template <typename T>
concept SizedRange = Range<T> && Sized<T>;
// Function constrained by combined concept
template <SizedRange T>
void processSizedRange(const T& container) {
std::cout << "Processing " << container.size() << " elements\n";
for (const auto& elem : container) {
std::cout << elem << " ";
}
std::cout << "\n";
}
// Simple requires clause (no named types)
template <typename T>
requires requires(T a, T b) { a + b; }
auto sum(T a, T b) {
return a + b;
}
int main() {
std::vector<int> v = {1, 2, 3};
processSizedRange(v); // Processing 3 elements
// std::forward_list<int> fl = {1, 2, 3};
// processSizedRange(fl); // Error: forward_list has no size()
}
Requires Expression Forms
Requires expressions can check four kinds of requirements:
#include <iostream>
#include <vector>
#include <concepts>
// 1. Simple requirement: expression must be valid
template <typename T>
concept HasBegin = requires(T t) {
t.begin(); // must compile
};
// 2. Type requirement: typename T::type must exist
template <typename T>
concept HasValueType = requires {
typename T::value_type; // type alias must exist
};
// 3. Compound requirement: expression + return type constraint
template <typename T>
concept Iterable = requires(T t) {
{ t.begin() } -> std::input_or_output_iterator;
{ t.end() } -> std::input_or_output_iterator;
};
// 4. Nested requirement: additional constraint
template <typename T>
concept ContiguousIterable = Iterable<T> && requires(T t) {
{ t.data() } -> std::contiguous_iterator;
};
// Full requires expression in one
template <typename T>
concept Container = requires(T t, const T ct, size_t i) {
typename T::value_type;
typename T::size_type;
typename T::iterator;
typename T::const_iterator;
{ t.begin() } -> std::same_as<typename T::iterator>;
{ ct.begin() } -> std::same_as<typename T::const_iterator>;
{ t.size() } -> std::same_as<typename T::size_type>;
t.swap(t);
// ...
};
int main() {
std::cout << ContiguousIterable<std::vector<int>> << "\n"; // 1
// std::cout << ContiguousIterable<std::list<int>> << "\n"; // 0
}
Concept Overloading
Concepts participate in overload resolution with a clear ordering.
#include <iostream>
#include <concepts>
#include <vector>
#include <list>
template <typename T>
requires std::integral<T>
void classify() {
std::cout << "Integral type\n";
}
template <typename T>
requires std::floating_point<T>
void classify() {
std::cout << "Floating-point type\n";
}
// Catch-all for arithmetic
template <typename T>
requires std::arithmetic<T>
void classify() {
std::cout << "Arithmetic type\n";
}
// More constrained overload is preferred
template <typename T>
requires std::integral<T> && (sizeof(T) == 1)
void classify() {
std::cout << "Single-byte integral type\n";
}
int main() {
classify<int>(); // Single-byte integral? No -> Integral type
classify<char>(); // Single-byte integral type
classify<double>(); // Floating-point type
// classify<std::string>(); // Error: no matching function
}
When multiple constrained templates match, the compiler selects the one with the "most constrained" requirements (subsumption rules).
Concepts vs SFINAE
Concepts produce dramatically better error messages.
#include <iostream>
#include <type_traits>
#include <concepts>
// SFINAE version (C++11-17)
template <typename T>
std::enable_if_t<std::is_integral_v<T>, void>
process_sfinae(T value) {
std::cout << "SFINAE: " << value << "\n";
}
// Concepts version (C++20)
template <std::integral T>
void process_concept(T value) {
std::cout << "Concept: " << value << "\n";
}
int main() {
process_sfinae(42); // Works
process_concept(42); // Works
// Uncomment to compare errors:
// process_sfinae("hello"); // ~20 lines of cryptic error
// process_concept("hello"); // ~3 lines: "constraints not satisfied"
}
Concepts also enable auto with constraints:
#include <iostream>
#include <concepts>
// Constrained auto parameters
void printNumber(std::integral auto value) {
std::cout << "Number: " << value << "\n";
}
// Constrained lambda (C++20)
auto add = []<std::integral T>(T a, T b) {
return a + b;
};
int main() {
printNumber(42); // OK
// printNumber("hello"); // Error
std::cout << add(3, 5) << "\n"; // 8
}
Real-World: Ranges Concepts
The Ranges library heavily uses concepts.
#include <iostream>
#include <ranges>
#include <vector>
#include <algorithm>
// Function constrained to work only with sorted, random-access ranges
template <std::ranges::random_access_range Rng>
requires std::ranges::sized_range<Rng>
bool binarySearch(const Rng& range, const std::ranges::range_value_t<Rng>& value) {
return std::ranges::binary_search(range, value);
}
int main() {
std::vector<int> v = {1, 3, 5, 7, 9};
std::cout << binarySearch(v, 5) << "\n"; // 1 (true)
std::cout << binarySearch(v, 6) << "\n"; // 0 (false)
}
Common Mistakes
Mistake 1: Confusing concept definition with requires clause
template <typename T>
concept C1 = requires(T t) { t.foo(); }; // Definition + requires expression
template <C1 T>
void func(T t) {} // Using concept
template <typename T>
requires requires(T t) { t.foo(); } // Requires clause with requires expression
void func2(T t) {}
The double requires is correct but confusing. requires(T t) { ... } is a requires expression; the first requires in requires requires is the requires clause keyword.
Mistake 2: Not understanding concept subsumption
template <std::integral T> void f(T); // More constrained
template <std::integral T> requires (sizeof(T) > 1) void f(T); // More constrained
Concepts subsume based on their definition, not arbitrary expressions. The sizeof version is more constrained only if the compiler can prove it.
Mistake 3: Using concepts only for error messages
Concepts also improve overload resolution, enable if constexpr checks with requires, and speed compilation by providing earlier failure.
Mistake 4: Over-constraining
template <typename T>
concept TooStrict = requires(T t) {
{ t.foo() } -> std::same_as<int>; // Must return exactly int
{ t.foo() } -> std::convertible_to<int>; // More flexible
};
Mistake 5: Forgetting that concepts are boolean predicates
template <typename T>
concept MyConcept = std::is_integral_v<T> && requires(T t) { ... };
Concepts must be constant expressions evaluable at compile time. They cannot depend on runtime values.
Practice Questions
- What is the output?
template <std::integral T> void f(T) { std::cout << "int"; }
void f(double) { std::cout << "double"; }
int main() { f(3.14); }
Answer: double — the non-template function is preferred over the constrained template.
What does a
requiresexpression check? Answer: It checks that certain expressions are valid, types exist, and return types satisfy constraints — all at compile time.Write a concept that checks if a type has a
length()method returningsize_t. Answer:
template <typename T>
concept HasLength = requires(const T& t) {
{ t.length() } -> std::same_as<size_t>;
};
Can a function template have multiple concepts on different parameters? Answer: Yes:
template <std::integral T, std::floating_point U> void func(T, U);.What happens when no constrained template matches? Answer: The compiler generates an error listing the constraints that were checked and how each failed, usually in a few concise lines.
FAQ
Mini Project
Implement a generic accumulate function that works only with ranges whose value type supports addition:
#include <iostream>
#include <vector>
#include <list>
#include <string>
#include <concepts>
// Your accumulate with concepts
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::cout << accumulate(v) << "\n"; // 15
std::list<double> lst = {1.5, 2.5, 3.0};
std::cout << accumulate(lst) << "\n"; // 7.0
std::vector<std::string> sv = {"hello", " ", "world"};
// accumulate(sv); // Works if string supports +
}
This project mirrors how real C++ libraries like std::ranges use concepts to Express type requirements clearly, and is directly comparable to Java interfaces as a constraint mechanism.
What's Next
You now use concepts to write readable, constrained templates. Next, you will explore type traits and template metaprogramming — techniques for computing types at compile time, building on everything from SFINAE to constexpr.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro