· 7 min read ·

The Simplification That Made C++ Concepts Work

Source: isocpp

Bjarne Stroustrup published a pedagogical paper on concept-based generic programming in November 2025, walking through the mechanics and idioms of C++20 concepts from first principles. It is a useful companion to the standard, particularly for the way it frames algorithms and type requirements as a unified design discipline. But the more interesting question the paper implicitly raises is not how concepts work, but why they work the way they do, specifically why C++ chose structural (implicit) satisfaction when nearly every comparable system in other languages chose explicit opt-in.

That choice was not obvious, and it was not the original plan.

The Concept That Was Too Ambitious

The original concepts design for what became C++11 was substantial. By 2007, it included not just constraint predicates but concept_maps, which were explicit declarations associating a type with a concept, and axioms, which encoded semantic requirements that the compiler could use for optimization. The design also paired concepts with early checking, meaning the template body would be type-checked against the concept’s requirements before instantiation rather than at instantiation time.

This is close to how Rust traits work, and close to how Haskell typeclasses work. In both systems, you write an explicit declaration that associates a type with an interface, and the compiler can check the generic function body in isolation before any instantiation occurs.

The C++ committee voted the whole design out of C++0x in July 2009. The specification had grown to hundreds of pages of normative wording. The GCC experimental implementation exposed surprising interactions. There was no consensus on the semantics of early checking for the parts of a template body that went beyond the concept’s declared requirements. The concept_map mechanism required the standard library to be massively annotated, and it raised a difficult question: who writes the concept map for a type you did not author, adapting it to a concept you also did not write?

Andrew Sutton, Stroustrup, and others returned with Concepts Lite, published as a Technical Specification in 2015. The simplification was drastic: concept_maps were dropped, axioms were dropped, early checking was dropped. What remained was a predicate-based constraint system with structural satisfaction, where a type models a concept if and only if the required expressions are well-formed and produce the required types, with no declaration needed.

That is the version that shipped in C++20.

What Structural Satisfaction Means in Practice

A C++ concept is a compile-time boolean predicate over type arguments. The requires expression is the mechanism that turns “does this expression compile?” into that boolean:

template <typename T>
concept Drawable = requires(T t) {
    { t.draw() } -> std::same_as<void>;
    { t.bounding_box() } -> std::convertible_to<Rect>;
};

Any type with a draw() method returning void and a bounding_box() returning something convertible to Rect automatically models Drawable. No declaration, no registration, no explicit opt-in. If the type has those methods, the concept is satisfied.

The contrast with Rust is the clearest illustration of the trade-off. In Rust, a type with a draw method cannot be used with a function constrained by a Drawable trait unless someone has written impl Drawable for ThatType. The explicit declaration is mandatory, and it prevents accidental satisfaction: a type that happens to have matching method signatures cannot be used unless someone consciously associated it with the trait.

C++ has no such barrier. A type that accidentally has the right methods satisfies the concept whether or not anyone intended it. This is occasionally a source of subtle bugs, but it provides a significant practical benefit: you can constrain generic code with concepts and use it against types from libraries you did not write, without modifying those types or writing any adapter code. The concept constrains the algorithm, not the type.

Haskell typeclasses require explicit instance declarations, as do Swift protocol conformances (extension MyType: Drawable { ... }). Go interfaces are the closest structural parallel to C++ concepts at the language level, but Go interfaces operate at runtime with vtable dispatch, whereas C++ concepts exist purely at compile time with no runtime representation.

The requires Expression

Four kinds of requirements can appear inside a requires expression.

Simple requirements check that an expression is well-formed: ++it; requires that pre-increment compiles. Type requirements check that a nested name exists: typename T::value_type; requires an associated type. Compound requirements check both well-formedness and the resulting type: { it++ } -> std::same_as<T>; requires that post-increment returns exactly T. Nested requirements apply a sub-constraint inline: requires std::copyable<T>; embeds another concept check.

Together these cover the syntactic requirements of any algorithm. They do not cover semantic requirements. A type can satisfy std::strict_weak_order<Comp, T, T> with a comparator that does not actually implement a strict weak ordering, and the compiler has no mechanism to detect this. The axiomatic layer of the original design, the part that could have addressed semantic requirements, was dropped in Concepts Lite. Stroustrup’s paper acknowledges this plainly.

The same limitation exists in Rust. Marker traits like Send and Sync carry semantic invariants that the programmer must ensure hold. Haskell’s typeclass laws are conventions documented in comments, not properties checked by the compiler. No current mainstream system fully solves the semantic verification problem at the language level; that work belongs to formal verification tools like Lean and Coq.

Subsumption Replaces Tag Dispatch

Subsumption is the overload resolution rule that makes concept-based dispatch clean. A template constrained by a more refined concept is preferred over one constrained by a less refined concept, because the more refined concept includes and extends the other’s requirements. The compiler selects the most constrained matching overload automatically.

Before C++20, selecting an algorithm implementation based on iterator category required tag dispatch:

template <typename I>
void advance_impl(I& it, int n, std::random_access_iterator_tag) {
    it += n;
}

template <typename I>
void advance_impl(I& it, int n, std::input_iterator_tag) {
    while (n--) ++it;
}

template <typename I>
void advance(I& it, int n) {
    advance_impl(it, n, typename std::iterator_traits<I>::iterator_category{});
}

With concepts, the overload set is direct:

template <std::input_iterator I>
void advance(I& it, int n) {
    while (n--) ++it;
}

template <std::random_access_iterator I>
void advance(I& it, int n) {
    it += n;
}

The std::random_access_iterator concept subsumes std::input_iterator because every random access iterator satisfies the input iterator requirements. The compiler selects the more constrained overload when available, falling back to the less constrained one otherwise.

The C++20 Ranges library uses this throughout. Every algorithm in <algorithm> has a std::ranges:: counterpart that is fully constrained, and the iterator hierarchy is expressed as a concept lattice:

input_iterator
  └── forward_iterator
        └── bidirectional_iterator
              └── random_access_iterator
                    └── contiguous_iterator

Algorithms are constrained to the minimum level they actually require, and the subsumption rules select the most efficient implementation for any given iterator type.

The Sentinel Concept and Range Design

One concrete design improvement that concepts enabled in the Ranges library is the sentinel_for<S, I> concept, which allows the end marker of a range to be a different type from the iterator. Before concepts, end() had to return the same type as begin() for compatibility with standard algorithms, which prevented representing ranges like null-terminated strings efficiently as first-class range types.

With sentinel_for, a sentinel that checks *ptr == '\0' rather than comparing pointer values is a valid end marker for a char* iterator. The constraint checks that the sentinel and iterator types support the required comparison operations, nothing more. This design would have been awkward to express cleanly without either concepts or a heavier object-oriented interface.

Ranges algorithms also accept projection parameters, callables that transform elements before comparison. The concept constraints ensure that the projected type satisfies the comparator’s requirements at the call site rather than inside the algorithm implementation. Using std::ranges::sort(people, {}, &Person::age) to sort by age member pointer works because the concepts verify at the call site that the projected type (int) satisfies std::strict_weak_order.

Compile Times and Diagnostics

The main practical improvement concepts deliver is diagnostic quality. A concept violation produces a clear error at the call site, naming the unsatisfied concept and which requirement failed. Before concepts, a type error inside a deeply instantiated template could produce fifty or more lines of output pointing at lines inside standard library implementations, requiring the programmer to trace back through several layers of template expansion to find the actual mistake.

The compile-time performance picture is more complicated. Concept constraint checking can short-circuit and reject an overload before partially instantiating a template body, which reduces work in some overload resolution scenarios. In practice, the C++20 Ranges library compiles measurably slower than classic <algorithm> for the same operations in many projects, because constrained overload sets require more constraint evaluation work per instantiation. The libc++ maintainers and GCC and Clang teams have ongoing work on constraint normalization and caching, and performance has improved since the initial C++20 release. The costs are real but bounded, and trending better.

The Design as a Position on Generic Programming

Stroustrup’s late 2025 paper is, read in retrospect, an argument that generic programming is most naturally expressed through constraints on observable behavior rather than through inheritance hierarchies or explicit declarations. An algorithm that requires something sortable should state that requirement directly in terms of the operations it needs, and any type providing those operations should work without modification.

The 2009 removal forced a simplification that clarified the underlying philosophy. Concept maps were the mechanism for explicit opt-in; dropping them committed C++ to the structural model. The result integrates cleanly with existing codebases, third-party types, and the accumulated library ecosystem, at the cost of the semantic guarantee that explicit registration provides.

Rust’s explicit traits give stronger guarantees and better tooling around trait bounds in exchange for requiring opt-in. C++ concepts give broader compatibility and zero-annotation integration in exchange for the possibility of accidental satisfaction and the absence of enforceable semantic requirements. Both represent coherent positions on the question of what a constraint system is for, and the difference between them is a genuine design disagreement, not an oversight on either side.

Was this interesting?