C++ - Concepts & Constraints
Overview
Estimated time: 60–80 minutes
Use C++20 concepts to express intent and constrain templates. Write readable error messages and safer overloads with requires
clauses, requires-expressions, and standard/custom concepts.
Learning Objectives
- Constrain templates with
requires
clauses and requires-expressions. - Use standard concepts like
std::integral
andstd::floating_point
. - Define simple custom concepts and apply them to functions and classes.
Prerequisites
Quick start: requires clause with a standard concept
#include <concepts>
#include <iostream>
template <std::integral T> // T must be an integral type
T add(T a, T b){ return a + b; }
int main(){
std::cout << add(2,3) << "\n"; // OK
// std::cout << add(2.5,3.1); // error: double is not integral
}
requires-expression: check available operations
#include <concepts>
#include <type_traits>
// has_plus: type T supports a + b returning T
template <class T>
concept has_plus = requires(T a, T b){
{ a + b } -> std::same_as<T>;
};
template <has_plus T>
T add_any(T a, T b){ return a + b; }
Custom concept: Sized range-like
#include <concepts>
#include <iterator>
#include <utility>
template <class R>
concept sized_iterable = requires(R r){
{ r.begin() };
{ r.end() };
{ r.size() } -> std::convertible_to<std::size_t>;
};
template <sized_iterable R>
auto first_or_default(const R& r){
return r.size() ? *r.begin() : typename R::value_type{};
}
Constrained overloads and abbreviated templates
#include <concepts>
#include <iostream>
void print(auto x) requires std::integral<decltype(x)> {
std::cout << "int:" << x << "\n";
}
void print(auto x) requires std::floating_point<decltype(x)> {
std::cout << "float:" << x << "\n";
}
int main(){ print(42); print(3.14); }
Expected Output:
int:42
float:3.14
Beginner Boosters
#include <concepts>
#include <string>
#include <iostream>
// Only accept strings that can be converted to size_t via size()
template <class S>
concept has_size = requires(const S& s){ { s.size() } -> std::convertible_to<std::size_t>; };
template <has_size S>
void show_size(const S& s){ std::cout << s.size() << "\n"; }
int main(){ std::string s = "hello"; show_size(s); }
Common Pitfalls
- Over-constraining: too strict concepts can reject valid types or overloads; prefer minimal constraints.
- Forgetting
#include <concepts>
and#include <type_traits>
for standard concepts/traits. - Ambiguous overloads when multiple constraints match; break ties or simplify overload set.
Checks for Understanding
- What’s the difference between a requires-clause and a requires-expression?
- How do concepts improve error messages compared to SFINAE?
Show answers
- A requires-clause guards a template/overload; a requires-expression checks validity of expressions and can specify return-type constraints.
- Constraints are named and readable; failed constraints report which concept failed, rather than deep template instantiation errors.
Exercises
- Create a
number_like
concept that accepts integral or floating-point types; overload a function fornumber_like
and test with int/double. - Write a
has_push_back
concept and a function that appends a value if supported.