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 and std::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

  1. What’s the difference between a requires-clause and a requires-expression?
  2. How do concepts improve error messages compared to SFINAE?
Show answers
  1. A requires-clause guards a template/overload; a requires-expression checks validity of expressions and can specify return-type constraints.
  2. Constraints are named and readable; failed constraints report which concept failed, rather than deep template instantiation errors.

Exercises

  1. Create a number_like concept that accepts integral or floating-point types; overload a function for number_like and test with int/double.
  2. Write a has_push_back concept and a function that appends a value if supported.