Thirty Years of Generic Programming: What C++ Concepts Finally Get Right
Source: isocpp
Bjarne Stroustrup published a paper on concept-based generic programming in late 2025, covered on isocpp.org, and it reads like a retrospective as much as a tutorial. The paper walks through programming techniques that illustrate how C++20 concepts should be used, not just how they work mechanically. It is worth reading for anyone who writes template-heavy C++, but it also raises questions the design cannot fully answer. The central one is the gap between what a concept can verify and what it needs to guarantee.
The Long Road to an Explicit Contract
C++ has had generic programming since the early 1990s. The template mechanism let you write algorithms parameterized over types, and the standard library shipped a large collection of them. But the model was entirely implicit. A std::sort required its iterator type to support random access, comparison, and swapping, and the compiler would tell you so only after instantiating the template and failing somewhere three layers deep in the implementation. The error messages were notorious for their uselessness.
The community developed workarounds. SFINAE (Substitution Failure Is Not An Error) let you write predicates that would reject types before instantiation, but the syntax was grotesque:
template<typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T val);
C++11 and C++14 added type traits, std::enable_if, and decltype, making the patterns slightly less painful but no less obscure. C++17’s if constexpr moved branching inside template bodies rather than at the interface level, which helped readability but did nothing for overload resolution or error quality.
Concepts were proposed for C++11. The C++0x draft had a full concepts design, including “concept maps” that would let you declare that an existing type satisfied a concept without modifying the type. The committee removed the entire feature in 2009 because the design was too complex and agreement on the semantics of concept maps was not reachable. It took another decade to produce the simpler design that landed in C++20.
The C++20 version dropped concept maps entirely. Satisfaction is structural: a type satisfies a concept if the required expressions are valid. No opt-in annotation needed.
What Concepts Do
A concept is a predicate over types, evaluated at compile time. You define one using a requires expression that lists the operations a type must support:
template<typename T>
concept Printable = requires(T x, std::ostream& os) {
{ os << x } -> std::same_as<std::ostream&>;
};
The braces-and-arrow syntax checks both that the expression is valid and that it returns a type satisfying the constraint on the right. This matters because a naive requires expression only checks well-formedness:
template<typename T>
concept HasSize = requires(T t) { t.size(); };
// Satisfied by anything with .size(), even if it returns void
The fix is to add a return-type constraint:
template<typename T>
concept HasSize = requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
};
Once you have concepts, you can constrain templates at the interface level:
template<typename T>
requires Printable<T>
void log(const T& val);
// Or using the abbreviated syntax
void log(const Printable auto& val);
Both forms are function templates. The abbreviated syntax looks like a regular function, which some find cleaner and others find misleading.
Concepts participate in overload resolution. When two constrained templates both match a call, the more constrained one wins. The compiler determines “more constrained” through concept subsumption: concept A subsumes B if A’s requirements, when fully expanded, include all of B’s requirements. This lets you write:
template<std::integral T> void f(T); // #1
template<std::signed_integral T> void f(T); // #2
f(-5); // selects #2, because signed_integral subsumes integral
f(5u); // selects #1
Subsumption with disjunctions is a known rough edge. If concept A is defined as B || C, A does not subsume B or C individually. The normalization rules for || stop the subsumption check from proceeding through disjunctions, which means you can write concepts that are arguably more specific but that the compiler cannot recognize as more constrained. It is a correctness decision, not an oversight, but it creates surprising behavior in practice.
The Structural vs. Nominal Question
Dropping concept maps in favor of structural satisfaction solved one problem and created another. The upside is simplicity: any type that has the right operations automatically satisfies the concept. You do not need to modify the type or add a declaration. This plays well with third-party types and generic code over types you do not control.
The downside is accidental satisfaction. A type with a begin() method that does something entirely unrelated to iteration will satisfy std::ranges::range. The name collision is real; it happens in large codebases where different teams build similar interfaces independently.
Rust takes the opposite position. A type satisfies a trait only if it has an explicit impl Trait for Type declaration. This is nominal satisfaction:
trait Printable {
fn print(&self);
}
struct Point { x: f64, y: f64 }
impl Printable for Point {
fn print(&self) { println!("({}, {})", self.x, self.y); }
}
Accidental satisfaction is impossible. If a type satisfies a trait, someone wrote it that way on purpose. The trade-off is that you cannot use a third-party type with a trait defined elsewhere unless you control at least one of them. Rust’s orphan rules enforce this: you can implement a foreign trait for a local type, or a local trait for a foreign type, but not a foreign trait for a foreign type.
Haskell typeclasses work similarly, with explicit instance declarations. The rules around instance coherence are more permissive than Rust’s, but the nominal opt-in is still required.
C++‘s structural approach was chosen partly for compatibility with decades of existing code and partly because the committee could not agree on concept maps. Whether it was the right choice depends on what you weight more: ergonomics when consuming third-party types, or safety against accidental satisfaction. Stroustrup’s paper advocates writing well-specified concepts with distinct operation names to reduce the risk, which is practical advice that does not fully resolve the tension.
The Axiom Problem
Concepts can verify that a type provides certain operations. They cannot verify that those operations satisfy any semantic properties. std::totally_ordered<T> requires that T support <, <=, >, >=, ==, and !=, but it cannot check that those operators are consistent with each other, that < is transitive, or that equality is an equivalence relation. A type can satisfy std::totally_ordered with a broken operator< that gives different results each call.
Stroustrup has written about “axioms” for years, going back well before C++20. An axiom is a semantic requirement that a concept imposes but cannot mechanically check. The concept documents that types satisfying it must obey certain mathematical properties; the programmer is expected to ensure compliance.
This is not unique to C++. Haskell’s Monad typeclass has well-known laws (left identity, right identity, associativity) that the type system does not enforce. Rust’s Ord trait is supposed to provide a total ordering, but the compiler will not stop you from implementing it incorrectly. The difference is that Stroustrup’s paper treats axioms as a first-class part of concept design, not an afterthought. A concept without documented axioms is an incomplete specification.
Some experimental work has explored connecting concepts to formal verification tools, allowing axioms to be checked for concrete types in test suites if not at compile time. Nothing like this has landed in the standard, but the paper’s emphasis on axioms pushes in that direction.
What Good Concept-Based Design Looks Like
The practical techniques in Stroustrup’s paper center on designing concepts that represent complete, coherent abstractions rather than ad hoc collections of required expressions. A useful concept should correspond to a mathematical structure or a well-understood programming interface. std::sortable, for example, requires not just that elements are comparable but that the comparison is a strict weak ordering. The concept name communicates the full requirement; the requires clause verifies the syntactic part of it.
Concept composition lets you build complex requirements from simpler ones:
template<typename T>
concept SortableRange =
std::ranges::random_access_range<T> &&
std::sortable<std::ranges::iterator_t<T>>;
This is clean and readable. The subsumption rules mean that SortableRange automatically subsumes random_access_range, so overloads on SortableRange will be preferred over overloads on random_access_range when both match.
For types that need to satisfy multiple concepts, the paper recommends thinking about concept hierarchies early in design rather than retrofitting constraints. A type that participates in range algorithms should be designed to satisfy range concepts from the start, not discovered to do so accidentally.
Comparing with the Alternatives
C++ concepts, Rust traits, and Haskell typeclasses all solve roughly the same problem: expressing constraints on generic code in a way that is checkable and participates in type-directed dispatch. The differences in design reflect different priorities.
Rust monomorphizes generic code, like C++, so both get zero-cost abstractions in the sense that generic calls compile to the same code as hand-written concrete calls. Rust also offers dyn Trait for runtime dispatch when you need heterogeneous collections, at the cost of a vtable lookup. C++ uses virtual functions for this; there is no direct equivalent to dyn Trait built on concepts, though type erasure patterns can approximate it.
Haskell defaults to dictionary-passing for typeclasses, which is runtime dispatch, though GHC inlines aggressively when types are known. The functional programming model makes the semantic laws more central to the culture; a Functor instance that violates the functor laws is considered broken by convention even though the compiler does not check.
Go’s interfaces are structural like C++ concepts, but they always use runtime dispatch. Go’s generics, added in 1.18, use interfaces as type constraints with structural satisfaction. There is no monomorphization; the compiler uses GC shape information to generate shared code. The result is simpler binaries but less optimization opportunity.
C++ sits in an unusual position: structural satisfaction with full monomorphization. It gets the ergonomics of the Go-style implicit opt-in with the performance of Rust’s generics. The cost is the accidental satisfaction problem and the inability to enforce semantic laws.
Where This Leaves Things
Stroustrup’s paper is a synthesis of how concepts should be used now that they exist, five years after C++20 shipped. The techniques it describes, careful concept design, explicit axioms, hierarchical composition, constrained overloads, represent mature practice that took time to develop. The earlier versions of this advice, scattered across blog posts and conference talks, are now in one place.
The structural satisfaction choice and the axiom problem are not going away. They reflect fundamental trade-offs in the design space, and the committee made a considered choice. The nominal approach in Rust and Haskell has real advantages, but it requires explicit annotation and orphan rules that C++ cannot impose on its existing codebase without breaking everything.
What the paper makes clear is that the quality of generic code depends less on which constraint mechanism you use and more on the quality of the concepts you design. A well-specified concept with clear axioms and a meaningful name is a design artifact as important as the algorithm it constrains. The language can check the syntactic half; the rest is still up to the programmer.