Skip to content

3. Concepts

What & why

A template's requirements used to be implicit: whatever the body happened to use. Misuse produced pages of errors deep inside the implementation. Concepts (C++20) make requirements explicit, checked at the call site, and part of overload resolution. They're the biggest usability improvement to templates since templates.

Using standard concepts

#include <concepts>

template <std::integral T>
T gcd(T a, T b) {
    while (b != 0) {
        a = std::exchange(b, a % b);
    }
    return a;
}

gcd(12, 18);     // OK
gcd(1.5, 2.0);
// error: no matching function for call to 'gcd(double, double)'
// note: constraints not satisfied ... the expression 'is_integral_v<T>' evaluated to 'false'

Four ways to write a constraint

// 1. Constrained template parameter
template <std::integral T>
T twice(T x);

// 2. requires clause after the template parameter list
template <typename T>
    requires std::integral<T>
T twice(T x);

// 3. Trailing requires clause
template <typename T>
T twice(T x) requires std::integral<T>;

// 4. Abbreviated function template
std::integral auto twice(std::integral auto x);

All four are equivalent here. Use (1) or (4) for simple cases; (2)/(3) when combining constraints with && / ||.

The standard concepts you'll use most

Header Concepts
<concepts> same_as, derived_from, convertible_to, integral, signed_integral, floating_point, equality_comparable, totally_ordered, copyable, movable, regular, invocable, predicate, constructible_from
<iterator> input_iterator, forward_iterator, bidirectional_iterator, random_access_iterator, contiguous_iterator, sortable, indirectly_readable
<ranges> ranges::range, ranges::sized_range, ranges::input_range, ranges::random_access_range, ranges::contiguous_range, ranges::view
#include <ranges>

double average(const std::ranges::input_range auto& r)
    requires std::floating_point<std::ranges::range_value_t<decltype(r)>> ||
             std::integral<std::ranges::range_value_t<decltype(r)>>
{
    double sum = 0;
    std::size_t n = 0;
    for (auto x : r) { sum += x; ++n; }
    return n ? sum / n : 0.0;
}

Defining your own concepts

A concept is a named compile-time predicate on types:

template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

requires expressions

Check that expressions are valid, and what they return:

template <typename T>
concept Shape = requires(const T& s) {
    { s.area() } -> std::convertible_to<double>;     // expression + return constraint
    { s.name() } -> std::convertible_to<std::string>;
};

template <typename T>
concept Container = requires(T c) {
    typename T::value_type;                          // type requirement
    { c.size() } -> std::convertible_to<std::size_t>;
    c.begin();                                       // simple requirement: must compile
    c.end();
    requires std::default_initializable<T>;          // nested requirement
};

template <typename T>
concept Hashable = requires(const T& x) {
    { std::hash<T>{}(x) } -> std::convertible_to<std::size_t>;
};

Now use them:

void print_area(const Shape auto& s) {
    std::cout << s.name() << ": " << s.area() << '\n';
}

struct Circle {
    double r;
    double area() const { return 3.14159 * r * r; }
    std::string name() const { return "circle"; }
};

print_area(Circle{1.0});   // OK — no inheritance required
print_area(42);            // error: 'int' does not satisfy 'Shape'

This is static duck typing with a checked contract — compare with virtual interfaces (runtime) from the intermediate track.

Ad-hoc requires requires

For one-off checks you can inline a requires expression in a requires clause:

template <typename T>
    requires requires(T x) { x.serialize(); }
void save(const T& x);

It's legal but ugly — name a concept instead if you'll use it twice.

Overloading on concepts

The compiler picks the most constrained viable overload:

#include <iterator>

template <std::input_iterator It>
void advance_by(It& it, std::iter_difference_t<It> n) {
    while (n-- > 0) ++it;
}

template <std::random_access_iterator It>          // subsumes input_iterator
void advance_by(It& it, std::iter_difference_t<It> n) {
    it += n;
}

A std::vector iterator satisfies both; random_access_iterator is defined in terms of (and therefore subsumes) input_iterator, so the second is more specialized and wins. Subsumption only works when constraints are built from the same named concepts combined with && — two different concepts with identical bodies don't subsume each other.

Constraining class templates and members

template <std::movable T>
class Queue {
public:
    void push(T value);

    // Only available when T is copyable
    Queue clone() const requires std::copyable<T>;

    // Only available when T can be printed
    void dump(std::ostream& os) const
        requires requires(std::ostream& o, const T& t) { o << t; };
};

Member functions with unsatisfied constraints simply don't exist for that instantiation — no error unless you call them.

Concepts with if constexpr and static_assert

template <typename T>
std::string describe(const T& x) {
    if constexpr (std::integral<T>) {
        return "integer " + std::to_string(x);
    } else if constexpr (Shape<T>) {
        return "shape " + x.name();
    } else {
        return "something";
    }
}

static_assert(Shape<Circle>);          // document and test that your type models a concept
static_assert(!Shape<int>);

Put static_assert(Concept<YourType>) next to types intended to satisfy a concept — it's a compile-time unit test.

Semantic requirements

Concepts check syntax, not meaning. std::totally_ordered requires < to compile, but can't verify that it's actually a total order. The standard concepts come with documented semantic requirements; violating them is undefined behavior in the algorithms that rely on them. Document yours too.

Designing good concepts

  • Model meaningful abstractions (Shape, Serializable, Allocator), not single operations (HasPlus).
  • Build on the standard concepts rather than reinventing them.
  • Keep the requirements minimal: exactly what your algorithms use.
  • Constrain at the interface; don't add requirements the implementation doesn't need.

Common mistakes

  • Constraining too little — the body uses operations the concept doesn't require; users still get deep errors.
  • Constraining too much — requiring std::regular when you only copy.
  • Expecting subsumption between different concepts with the same body.
  • requires in the wrong place — requires std::integral<T> after the parameter list vs. requires requires for an expression.
  • Forgetting const& in requires-expression parameters, then rejecting const types.
  • Using concepts for runtime polymorphism — they're compile-time only; a std::vector of "anything satisfying Shape" still needs type erasure or std::variant.

Exercises

  1. Rewrite the SFINAE examples from lesson 2 (half, has_size) with concepts.
  2. Define concept Printable (works with std::ostream& << x) and write print_all(const Printable auto&... xs).
  3. Define a Shape concept and three unrelated structs that model it. Add static_asserts. Write total_area(const std::vector<S>&) constrained on Shape<S>.
  4. Write two overloads of distance(first, last): one for std::input_iterator, one for std::random_access_iterator. Verify which is called for a std::list and a std::vector.
  5. Write class Cache<K, V> that requires K to be Hashable and std::equality_comparable, with a dump() member available only when both are Printable.
  6. Pass a type that almost satisfies your concept (missing const on a method) and read the diagnostic. Compare it with the error from an unconstrained template.

Recap

  • Concepts are named compile-time predicates; they constrain templates and improve errors.
  • Four equivalent syntaxes; template <Concept T> and Concept auto for simple cases.
  • requires (...) { expr; { expr } -> concept; typename T::x; requires C<T>; } defines requirements.
  • Overload resolution prefers the most constrained overload via subsumption.
  • Constrained members exist only when their constraints hold.
  • Concepts check syntax; semantics are your documented responsibility.

Next: constexpr & compile-time programming →