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 requiresclauses and requires-expressions.
- Use standard concepts like std::integralandstd::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_likeconcept that accepts integral or floating-point types; overload a function fornumber_likeand test with int/double.
- Write a has_push_backconcept and a function that appends a value if supported.