Concepts
Concepts, added in C++20, let a template state its requirements on a type directly and readably in its own signature — turning a silent assumption buried in a template's body into an explicit, compiler-checked, and clearly diagnosed part of its interface.
Before C++20, a template's requirements on its type parameters lived nowhere but the template's own body — my_max<T> needs operator>, my_find's iterator needs !=, ++, and *, but neither signature says so. Call either one with an unsuitable type and the compiler discovers the problem deep inside the template's implementation, often producing a wall of substitution-failure errors that never actually names the missing capability in plain language. Concepts fix this by giving that requirement a name, checked directly against the template argument before the body is even considered. This page covers what a concept actually is, the requires clause and requires expression syntax for writing and checking them, the abbreviated constrained-template syntax (including the exact template <std::integral T> T add(T a, T b) form), the standard library's own ready-made concepts, how to write a custom one, how concepts compare to the older SFINAE-based technique they largely replace, and why the compiler errors concepts produce are dramatically easier to read.
The Problem Concepts Solve
An ordinary, unconstrained template accepts any type at all as far as its signature is concerned — the actual requirements only surface when the compiler tries to compile the template body with that specific type substituted in, and fails partway through.
Nothing about add's declaration signals that T needs operator+ — a caller (or a reader) has to read the entire body to discover that requirement, and the compiler's own error message, generated from deep inside a failed instantiation, is often long and indirect rather than a clear, one-line statement of what went wrong.
template <typename T>
T add(T a, T b)
{
return a + b;
}
struct Point
{
int x, y;
};
// add(Point{1, 2}, Point{3, 4});
// error occurs deep inside add's body, on the line "return a + b;",
// because Point has no operator+ — the error doesn't point at add's
// signature or clearly say "Point isn't addable"What Is a Concept?
A concept is a named, compile-time predicate over one or more types — it evaluates to true or false for a given type, based on constraints you write once and can then reuse across any number of templates. Concepts are declared with the concept keyword, and a boolean expression (frequently a requires expression, covered next) after an =.
Addable<T> is true for exactly the types T for which the expression a + b is well-formed given two values of type T — int, double, and std::string all satisfy it; a Point struct with no operator+ does not.
#include <concepts>
template <typename T>
concept Addable = requires(T a, T b) {
a + b;
};requires Clauses and requires Expressions
There are two related but distinct uses of the word requires. A requires clause attaches a constraint to a template declaration, restricting which types it can be instantiated with. A requires expression (seen just above, inside the Addable concept) is itself a boolean expression that checks whether certain syntax compiles for a given type — it's the tool most concepts are actually built out of.
The trailing requires std::integral<T> clause on add is checked before the template is ever instantiated — calling add(3, 4) succeeds because int satisfies std::integral, while calling add(3.0, 4.0) is rejected immediately, with a message naming exactly which constraint failed. The requires expression inside HasSize lists the syntax T must support; each line inside the braces is a compile-time check, not code that actually runs.
#include <concepts>
// requires clause: constrains T using an existing concept
template <typename T>
requires std::integral<T>
T add(T a, T b)
{
return a + b;
}
// requires expression: checks that specific operations compile for T
template <typename T>
concept HasSize = requires(T t) {
t.size(); // must compile
{ t.size() } -> std::convertible_to<std::size_t>; // and must convert to size_t
};Constrained Templates: the Abbreviated Syntax
Writing out a full requires clause on every constrained template gets verbose, so C++20 also allows a concept to be used directly in place of typename in the template parameter list — the constrained-parameter form. This is the most common way concepts appear in real code.
template <std::integral T> T add(T a, T b) means exactly the same thing as the trailing-requires-clause version from the previous section — std::integral<T> is checked as part of overload resolution and template argument deduction, before the body is ever considered, so a caller passing a double gets an immediate, clear rejection naming the failed constraint rather than an error from inside add's body.
There's a third equivalent form too, using auto parameters directly with a concept — useful for very short, single-expression constrained functions.
#include <concepts>
#include <iostream>
template <std::integral T>
T add(T a, T b)
{
return a + b;
}
int main()
{
std::cout << add(3, 4) << "\n"; // OK: int satisfies std::integral
// add(3.0, 4.0); // error: double does not satisfy std::integral
return 0;
}// Equivalent again, using an abbreviated function template with auto
std::integral auto add(std::integral auto a, std::integral auto b)
{
return a + b;
}Standard Concepts
The <concepts> header ships a substantial library of ready-made concepts covering the most common constraints generic code needs, so most everyday uses of concepts don't require writing a custom one at all.
std::integral, std::floating_point, std::same_as, std::convertible_to, and std::equality_comparable are among the most frequently used standard concepts; each captures a requirement that used to be expressed with ad hoc type traits and enable_if, in a name that reads like ordinary English.
#include <concepts>
std::integral<int>; // true — int is an integer type
std::floating_point<double>; // true
std::same_as<int, int>; // true — exact type match
std::convertible_to<int, double>; // true — int converts to double
std::equality_comparable<std::string>; // true — supports == and !=
std::default_initializable<std::vector<int>>; // true — has a default constructorCustom Concepts
When the standard library's concepts don't capture what a template actually needs, a custom concept can combine standard concepts, requires-expression syntax checks, and ordinary boolean logic into a new, meaningfully named constraint.
Printable<T> is true exactly for types that can appear on the right-hand side of os << value — int and std::string both qualify because operator<< is defined for them. logValue is now explicit about needing a printable type, and passing something without operator<< would be rejected with a message naming the Printable constraint directly.
#include <concepts>
#include <iostream>
#include <ostream>
template <typename T>
concept Printable = requires(T value, std::ostream &os) {
os << value; // must compile: value can be streamed to an ostream
};
template <Printable T>
void logValue(const T &value)
{
std::cout << "[log] " << value << "\n";
}
int main()
{
logValue(42);
logValue(std::string("ready"));
return 0;
}Concepts vs SFINAE
Before concepts, the same kind of constraint was expressed with SFINAE (Substitution Failure Is Not An Error) — typically std::enable_if, which removes an overload from consideration by making its return type ill-formed for types that don't satisfy some type trait. It works, but it's dense, indirect, and easy to get subtly wrong.
add_old works by exploiting a rule about substitution failure in an unused second template parameter — nothing about that parameter's purpose is visible without already knowing the enable_if idiom by heart. add_new states the identical constraint directly and legibly as std::integral T, with no indirect tricks involved.
#include <type_traits>
// Pre-C++20 SFINAE version, constraining T to integral types
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
T add_old(T a, T b)
{
return a + b;
}
// C++20 concept version — same constraint, far more readable
template <std::integral T>
T add_new(T a, T b)
{
return a + b;
}Improving Compiler Diagnostics
The single biggest practical benefit of concepts is diagnostic quality. A SFINAE failure typically removes an overload from consideration silently, then reports that no matching overload was found at all, without saying why any particular candidate was rejected. A concept failure, by contrast, is checked explicitly and reported by name.
The compiler names the exact concept that failed — std::integral<double> — rather than surfacing a generic overload-resolution failure or a syntax error from deep inside add's body. That single extra sentence of diagnostic can save significant debugging time on a large, heavily templated codebase.
Best practice: prefer a standard concept (std::integral, std::floating_point, std::totally_ordered, and the rest) whenever one already expresses what you need, reach for a custom concept when you need a project-specific requirement, and give custom concepts names that read naturally in the template signature — template <Printable T>, template <Sortable T> — so the constraint documents the function's intent as well as enforcing it.
What problem do concepts solve that unconstrained templates before C++20 didn't handle well?
An unconstrained template's requirements on its type parameters live only in its body, invisible from its signature — calling it with an unsuitable type produces an error from deep inside a failed instantiation, often as a long, indirect message that doesn't clearly name what went wrong. Concepts let a template state its requirements explicitly and checkably in its own signature, so a mismatch is caught immediately with a diagnostic naming the exact failed constraint.
What is the difference between a requires clause and a requires expression?
A requires clause attaches a constraint to a template declaration (for example, template <typename T> requires std::integral<T>), restricting what types the template accepts. A requires expression is itself a boolean expression — the thing being checked — that verifies certain syntax compiles for a given type; most concepts are defined using one.
Write out (in words) what template <std::integral T> T add(T a, T b) { return a + b; } means, and name two other equivalent ways to express the same constraint.
It declares a function template add that only accepts types satisfying the std::integral concept, meaning only integer types like int, long, or char may be substituted for T. The same constraint can be written with a trailing requires clause (template <typename T> requires std::integral<T> T add(T a, T b) {...}) or with an abbreviated function template using std::integral auto parameters directly (std::integral auto add(std::integral auto a, std::integral auto b) {...}).
How do concepts compare to the older SFINAE-based std::enable_if technique for constraining a template, in terms of what they let you express?
Both can restrict a template to types satisfying some condition, but SFINAE achieves this indirectly, typically by making an unused template parameter or return type ill-formed for types that don't satisfy a trait — a dense idiom that has to be learned by heart to read. Concepts express the identical constraint directly and by name (template <std::integral T>), and are checked explicitly rather than exploited as a side effect of substitution rules.
Why do concept-constrained templates typically produce better compiler error messages than SFINAE-constrained ones?
A SFINAE failure usually removes an overload from consideration silently and the compiler reports only that no matching overload was found, without saying which candidate was rejected or why. A concept failure is checked and reported explicitly, naming the exact concept that evaluated to false for the given type (for example, std::integral<double> evaluated to false), which points directly at the real problem.
Write a custom concept Comparable<T> requiring that two values of type T support both < and ==, and a constrained function template inRange(T value, T low, T high) that uses it to check whether value falls within [low, high].
Comparable<T> checks that a < b and a == b both compile and convert to bool for T — true for int and double via built-in operators. inRange is constrained to only accept such types, so calling it with a type lacking those operators would be rejected immediately with a diagnostic naming the Comparable constraint, rather than failing deep inside inRange's body.
Why does totalArea(c) compile but the commented-out totalArea(Point{1.0, 2.0}) call would fail, and what would the compiler's diagnostic point at?
totalArea(c) compiles because Circle has an area() method returning something convertible to double, so Circle satisfies the HasArea concept. Point has no area() method at all, so it fails the requires expression inside HasArea — the compiler rejects the call with a diagnostic naming HasArea<Point> as unsatisfied, rather than an error about a missing member function buried inside totalArea's body.
Rewrite this pre-C++20 SFINAE function using a C++20 concept instead, keeping the same behavior (only accepting floating-point types): template <typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>> T half(T value) { return value / 2; } template <std::floating_point T> T half(T value) expresses exactly the same constraint as the enable_if version — only float, double, and long double may be substituted for T — but directly and legibly, using a standard concept instead of an unused, exploited second template parameter. Calling half(9) with an int now fails with a diagnostic naming std::floating_point<int> as unsatisfied, rather than a cryptic substitution-failure message.
template <std::integral T>
T add(T a, T b) { return a + b; }
// add(3.0, 4.0);
// error: constraints not satisfied
// note: the required expression 'std::integral<double>' evaluated to false#include <concepts>
template <typename T>
concept HasArea = requires(T shape) {
{ shape.area() } -> std::convertible_to<double>;
};
struct Circle { double radius; double area() const { return 3.14159 * radius * radius; } };
struct Point { double x, y; };
template <HasArea T>
double totalArea(T shape) { return shape.area(); }
int main()
{
Circle c{2.0};
totalArea(c);
// totalArea(Point{1.0, 2.0});
return 0;
}