The Semantic Contract That SFINAE Never Had: C++20 Concepts in Practice
Source: isocpp
The paper Bjarne Stroustrup published in late 2025 is not a proposal or a feature announcement. It is a tutorial on generic programming using C++20 concepts, written by the designer of C++ himself. Reading it retrospectively, what stands out is how consistently the paper treats concepts as a semantic tool rather than a syntactic convenience. Concepts are not a cleaner way to write SFINAE. They are a different theory about what constraints on generic code should express.
The Problem That Needed a Different Frame
Generic programming in C++ before C++20 relied on substitution failure. SFINAE (Substitution Failure Is Not An Error) enabled templates that silently disabled themselves based on type properties:
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
T double_it(T x) {
return x * 2;
}
The constraint here is expressed as a side effect of a failed substitution, not as a declared requirement. When it fails, the compiler produces error messages that trace through layers of template instantiation before reporting that no matching function was found. Anyone who has spent time deciphering a wall of Boost.MPL errors knows the cost in debugging time.
Concepts change the frame. A concept is a predicate over types, stated once and checked at the point of use:
template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;
template<Arithmetic T>
T double_it(T x) {
return x * 2;
}
The constraint is named, readable, and produces a direct diagnostic if violated. More importantly, the constraint becomes part of the interface. Callers can read what is required without inspecting the implementation.
A Decade in Waiting
Concepts nearly shipped with C++11. An early proposal by Stroustrup and collaborators was ambitious but too large, and the committee rejected it in 2009. What remained was the <type_traits> machinery and the SFINAE idioms that accumulated around it over the following decade.
The second attempt arrived as Concepts Lite, later formalized as the Concepts TS (ISO/IEC TS 19217, 2015). This version was deliberately smaller, focused on constraint checking without attempting to encode axioms or semantic invariants. GCC implemented it early. The design proved usable, compile-time improvements were measurable, and after further refinement it entered C++20.
The decade-long delay had a real cost. A generation of C++ programmers learned SFINAE and built libraries around it. Codebases that predate C++20 mix both styles, and the friction at those boundaries is still visible in production codebases today.
What the Paper Is Actually Arguing
Stroustrup’s 2025 paper approaches concepts as a pedagogical design exercise. It walks through building a small type system, starting from unconstrained templates and progressively adding constraints as the design reveals what the code actually requires from its type parameters. The key insight it keeps returning to is that concept design is a process of discovering what you mean semantically, not just what operations a type needs to support mechanically.
This framing differs from most concepts tutorials, which focus on syntax: here is requires, here is a concept definition, here is abbreviated function template notation. Stroustrup’s paper asks what a concept represents as an abstraction. What does it mean for a type to be Regular? To be EqualityComparable? The standard library concept hierarchy is a vocabulary for answering those questions precisely.
The Standard Library concepts demonstrate this well:
// Regular: copyable, default constructible, equality comparable
template<typename T>
concept Regular = std::semiregular<T> && std::equality_comparable<T>;
// An algorithm stating exactly what it needs from iterators
template<std::input_iterator I, std::sentinel_for<I> S>
auto count_if(I first, S last, auto pred) -> std::iter_difference_t<I>;
The std::input_iterator concept encodes a model of sequential access: the type must be dereferenceable, incrementable, and the result of dereferencing must be readable. These requirements map to a mathematical model of iteration, not merely a list of required member functions.
Concepts in Comparison
The comparison to Rust traits is instructive. Rust traits require an explicit implementation declaration:
trait Scalable {
fn double(self) -> Self;
}
impl Scalable for i32 {
fn double(self) -> Self { self * 2 }
}
C++ concepts are structural: a type satisfies a concept if it supports the required operations, with no explicit declaration needed. This allows concepts to work with existing types retroactively, but the relationship between a type and a concept is never recorded in the type definition itself. Rust’s nominal approach makes satisfying a trait a deliberate act and makes trait implementations searchable. Both designs reflect legitimate priorities; the tradeoff is retroactive applicability versus verifiable intent.
Haskell typeclasses are the clearest intellectual ancestor of concepts. They constrain polymorphic functions in the same way, and the standard library’s concept hierarchy borrows from Haskell’s numeric and ordering class hierarchy. Haskell enforces global coherence, permitting only one instance of a typeclass per type, while C++ permits multiple concept definitions and specializations without coordination. This gives C++ more flexibility at the cost of potential inconsistency across libraries.
Go interfaces occupy a different point in the design space. They are implicitly satisfied like C++ concepts but are primarily intended for runtime dispatch rather than compile-time constraint checking. The ergonomics are simpler, but the expressiveness is narrower: expressing “a type that supports addition and multiplication” as a Go interface requires a method contract that maps awkwardly to numeric operators, and there is no mechanism for overload selection based on constraint specificity.
Compile-Time Implications
One concrete argument for concepts is compile-time performance. SFINAE generates template instantiation attempts for each overload candidate, and as the number of overloads grows the cost scales poorly. Concepts check a constraint before instantiation and fail fast, avoiding the recursive instantiation work entirely.
Codebases that have migrated from SFINAE to concepts in heavily templated code report compile-time reductions in the range of 10 to 40 percent. The gains are most visible in math libraries, ranges pipelines, and expression template systems, where overload sets tend to be large and deeply nested. The error message improvement is harder to quantify but easier to experience: a concept violation produces a diagnostic pointing at the failed constraint, while SFINAE failures produce instantiation stack traces that can run to hundreds of lines.
Constraint Subsumption
One aspect of concepts that deserves more attention is constraint subsumption. When two overloaded function templates both match a call, the compiler can automatically select the more constrained one:
template<std::input_iterator I>
void advance(I& it, int n) {
// Linear: step one at a time
while (n--) ++it;
}
template<std::random_access_iterator I>
void advance(I& it, int n) {
// Constant time: direct offset
it += n;
}
std::random_access_iterator subsumes std::input_iterator in the standard concept hierarchy, so the compiler selects the more efficient overload when the type supports it. Before concepts, this pattern required tag dispatch or explicit partial specializations, both of which are more verbose and more fragile at the call site.
Designing concept hierarchies that support subsumption requires care. Concepts subsume each other only through conjunction and disjunction of atomic constraints, not through arbitrary requires expressions. A concept defined as template<typename T> concept Foo = Bar<T> && Baz<T> participates in subsumption reasoning; a concept built from a complex inline requires block may not. This distinction matters when you want the compiler to prefer more constrained overloads automatically.
The Current State
All three major compilers support C++20 concepts at production quality: GCC since version 10, Clang since version 10, MSVC since Visual Studio 2019 version 16.3. The cppreference constraints reference covers the full syntax and semantics. The standard library concepts in <concepts> and <iterator> are stable and well-tested across platforms.
The remaining friction is mostly at legacy boundaries. Libraries written before C++20 use SFINAE throughout, and bridging between the two styles requires care. The pragmatic migration path is gradual: identify the semantically meaningful constraints in existing SFINAE, name them as concepts, and introduce the new style at new API boundaries first. Mechanical substitution of enable_if with requires without reconsidering the underlying semantics misses the point of the feature.
Stroustrup’s paper is worth reading as a design document, not just a reference. Its argument is that generic programming has always been about mathematical abstractions, and that concepts provide syntax which matches the intent of those abstractions directly. The SFINAE approach was a workaround built on language machinery that was never designed for expressing constraints; the standard library accumulated decades of complexity to make that workaround usable at scale. Concepts close that gap, and the paper is a careful accounting of what the language can now express that it previously could not.