Introduction

Templates are the backbone of generic programming in C++, but unconstrained templates often produce cryptic compiler errors and allow unintended types to slip through. Since C++20, concepts give us a expressive, readable way to state requirements directly in the code. I reach for them whenever I write a library component that must work with a family of types — containers, iterators, callables, you name it.

Why Concepts Matter

Before concepts, we relied on SFINAE tricks or static_asserts buried deep in the implementation. Those approaches work, but they scatter the contract across the code base and produce error messages that read like a novel. A concept centralizes the requirement, makes it part of the interface, and lets the compiler emit a concise diagnostic the moment a caller violates it.

Think of a concept as a compile‑time predicate that documents *what* a type must do, not *how* it does it.

Defining a Simple Concept

Let's start with a classic: a type that can be streamed to std::ostream.


#include 
#include 

// A concept that checks for the existence of operator<<
template 
concept Streamable = requires (std::ostream& os, const T& value) {
    { os << value } -> std::convertible_to;
};

The requires expression enumerates the operations we need. The trailing -> std::convertible_to ensures the expression returns something compatible with std::ostream&, matching the usual chaining behavior.

Using Concepts in Function Templates

Now the concept becomes a constraint on a template parameter:


void print_any(const Streamable auto& obj) {
    std::cout << obj << '\n';
}

The abbreviated function template syntax (const Streamable auto&) is just syntactic sugar for template void print_any(const T&). Both forms are equivalent; pick the one that reads better in context.

Combining Concepts

Real‑world code often needs several properties at once. Concepts compose naturally with logical operators:


template 
concept Incrementable = requires (T& x) {
    { ++x } -> std::same_as;
    { x++ } -> std::same_as;
};

template 
concept Comparable = requires (const T& a, const T& b) {
    { a == b } -> std::convertible_to;
    { a < b } -> std::convertible_to;
};

template 
concept Sortable = Incrementable && Comparable;

Sortable now expresses exactly the capabilities a type must have to work with a simple bubble‑sort implementation.

Real‑World Example: A Generic Sort Wrapper

Imagine a utility header used across a code base that provides a quick_sort overload for any random‑access range. With concepts we can constrain the iterator type and the value type in a single, readable signature:


#include 
#include 
#include 

// Require a random‑access iterator whose value type is Sortable
template 
requires Sortable>
void quick_sort(Iter first, Iter last) {
    if (first >= last) return;
    auto pivot = *std::next(first, std::distance(first, last) / 2);
    Iter i = first, j = last - 1;
    while (i <= j) {
        while (*i < pivot) ++i;
        while (pivot < *j) --j;
        if (i <= j) {
            std::iter_swap(i, j);
            ++i;
            if (j == first) break; // avoid underflow
            --j;
        }
    }
    quick_sort(first, j + 1);
    quick_sort(i, last);
}

If a caller passes a std::list iterator, the compiler instantly reports that std::list::iterator does not model std::random_access_iterator. If they pass a vector of a type lacking operator<, the diagnostic points to the missing Comparable requirement. No more scrolling through pages of template instantiation backtraces.

Pitfalls and Best Practices

  • Don't over‑constrain. Only require what the algorithm truly needs. Extra constraints reduce flexibility and increase compilation time.
  • Prefer standard library concepts. std::integral, std::floating_point, std::ranges::range, etc., are well‑tested and familiar to other developers.
  • Keep concepts in a dedicated header. This mirrors the way you'd organize type traits and makes them easy to reuse across translation units.
  • Document the semantic intent. A concept name like Drawable tells a reader more than HasDrawMethod.

Conclusion

Concepts turn template contracts from hidden implementation details into first‑class, self‑documenting API elements. They catch misuse at the call site, produce readable errors, and let you express complex type relationships with a few lines of declarative code. The next time you reach for enable_if or a cryptic static_assert, ask yourself whether a concept would make the intent clearer — chances are, it will.