Concepts
Before C++20, the only way to constrain templates by type was SFINAE and enable_if — a powerful mechanism, but horrifying to use. A single mistake produced pages-long walls of compiler messages, and the code itself turned into riddles like std::enable_if_t<std::is_integral_v<T> && !std::is_same_v<T, bool>, T>. Concepts arrived in C++20 precisely to make type constraints readable, diagnosable, and semantically explicit.
A concept is a named compile-time predicate: a boolean condition that a type either satisfies or does not. Think of it as a contract: a function with the concept std::integral<T> tells both the compiler and the human — "I accept only integers." Pass a std::string and the compiler says exactly that, instead of emitting a wall of indirect substitution errors.
Defining a concept
#include <concepts>
#include <iostream>
template<typename T>
concept Printable = requires(T val) {
{ std::cout << val }; // T must support operator<<
};
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<typename T>
concept HasSize = requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
};
A concept is defined with the concept keyword after template<typename T>. The right-hand side is a compile-time boolean expression. It may be compound (&&, ||), reference other concepts, or contain a requires expression.
requires — two different beasts
The word requires plays two completely different roles in C++20. They are easy to confuse.
requires clause — constraining a template
A requires clause sits outside the definition and states the condition under which the template participates in overload resolution:
// Way 1: requires clause after the template parameter list
template<typename T>
requires std::integral<T>
T gcd(T a, T b) {
while (b) { a %= b; std::swap(a, b); }
return a;
}
// Way 2: concept directly in the template parameter (shorthand syntax)
template<std::integral T>
T gcd(T a, T b) { /* ... */ }
// Way 3: trailing requires (after the function signature)
template<typename T>
T gcd(T a, T b) requires std::integral<T> { /* ... */ }
All three forms are semantically identical. The shorthand syntax (template<std::integral T>) is the most readable and the preferred choice for a single concept.
requires expression — checking a set of expressions
A requires expression sits inside a concept definition (or directly in a requires clause) and describes which operations must be valid for the type:
template<typename T>
concept Sortable = requires(T& container) {
container.begin(); // the expression must compile
container.end();
typename T::value_type; // the nested type must exist
{ container.size() } -> std::unsigned_integral; // the type of the expression's result
};
Inside requires { ... } every line is a static check, not executable code. { expr } -> Concept means: the expression expr must compile, and its type must satisfy Concept. The body of a requires expression never runs at runtime.
You can use a requires expression directly in a requires clause, without declaring a separate concept:
// Ad-hoc constraint: no named concept, we just require
template<typename T>
requires requires(T a, T b) { a + b; }
T add(T a, T b) { return a + b; }
The double requires — the first is the clause, the second is the expression. It looks odd, but it is legal.
SFINAE vs Concepts
call to 'square(const char*)'
note: candidate: template<...>
note: template argument deduction
note: substitution failure [...]
note: ...
satisfy constraint 'integral'
+ documents intent
Four ways to apply a concept
// 1. Concept directly in the template parameter (most readable)
template<Printable T>
void print(T val) { std::cout << val; }
// 2. requires clause
template<typename T> requires Printable<T>
void print(T val) { std::cout << val; }
// 3. Trailing requires (after the signature)
template<typename T>
void print(T val) requires Printable<T> { std::cout << val; }
// 4. Abbreviated auto parameters (abbreviated function templates)
void print(Printable auto val) { std::cout << val; }
The fourth way — abbreviated function templates — is the most compact. The compiler automatically turns Printable auto val into a template parameter. Each auto parameter introduces an independent template parameter:
// These two forms are equivalent:
void swap_values(std::integral auto a, std::integral auto b);
// ≡
template<std::integral T, std::integral U>
void swap_values(T a, U b);
// T and U are independent types! If you need a single type, use an explicit template.
Compound concepts
Concepts are combined with && and ||:
template<typename T>
concept SignedIntegral = std::integral<T> && std::signed_integral<T>;
template<typename T>
concept StringLike = std::same_as<T, std::string>
|| std::same_as<T, std::string_view>
|| std::convertible_to<T, std::string_view>;
template<typename T>
concept Container = requires(T c) {
c.begin(); c.end(); c.size();
typename T::value_type;
typename T::iterator;
} && std::copyable<T>;
An important subtlety about && in concepts: the logical operators && and || in concepts are not the same operators as in ordinary code. They have no short-circuit semantics in the general sense, yet for concept subsumption the compiler understands them structurally. More on this in the subsumption section below.
Standard concepts (``)
Types and conversions
std::same_as<T, U> // T and U are the same type
std::derived_from<D, B> // D publicly derives from B
std::convertible_to<From, To> // From implicitly converts to To
std::common_with<T, U> // T and U have a common type (std::common_type_t)
std::common_reference_with<T, U>
Object semantics
std::destructible<T> // ~T() does not throw
std::constructible_from<T, Args...>
std::default_initializable<T>
std::move_constructible<T>
std::copy_constructible<T>
std::movable<T> // movable + swap
std::copyable<T> // copyable + movable
std::semiregular<T> // copyable + default_initializable
std::regular<T> // semiregular + equality_comparable
std::regular<T> is the minimal set for a "normal" data type: it can be copied, moved, default-constructed, and compared for equality. This is exactly what the standard expects from types that behave like "values."
Comparisons
std::equality_comparable<T>
std::equality_comparable_with<T, U>
std::totally_ordered<T>
std::totally_ordered_with<T, U>
Callable
std::invocable<F, Args...> // F can be called with Args...
std::regular_invocable<F, Args...>// invocable + pure function (no side effects)
std::predicate<F, Args...> // invocable, returns bool
std::relation<R, T, U> // binary relation
std::strict_weak_order<R, T, U> // strict weak order (needed by std::sort)
Numeric types
std::integral<T> // integer type (including bool, char)
std::signed_integral<T> // signed integer
std::unsigned_integral<T> // unsigned integer
std::floating_point<T> // float, double, long double
Note: std::integral<bool> is true, std::signed_integral<bool> is false.
Ranges (``)
std::ranges::range<R> // R has begin() and end()
std::ranges::sized_range<R> // + size() in O(1)
std::ranges::bidirectional_range<R>
std::ranges::random_access_range<R>
std::ranges::contiguous_range<R> // elements are laid out contiguously in memory
std::ranges::viewable_range<R> // R can be passed to views::all()
Iterator hierarchy (``)
std::input_iterator<I>
↓
std::forward_iterator<I> // multi-pass, comparable
↓
std::bidirectional_iterator<I> // + operator--
↓
std::random_access_iterator<I> // + operator[], +n, -n, <, >, <=, >=
↓
std::contiguous_iterator<I> // elements in contiguous memory (std::to_address)
template<std::random_access_iterator It>
void my_sort(It begin, It end) {
// The algorithm requires random access — std::list won't qualify
std::sort(begin, end);
}
Subsumption — the more constrained concept wins
Subsumption is the rule for resolving overload ambiguity: if one template imposes stricter constraints than another, the stricter one wins without an ambiguous overload.
template<typename T>
concept Integral = std::integral<T>;
template<typename T>
concept SignedIntegral = Integral<T> && std::signed_integral<T>;
// Two candidates:
template<Integral T> void process(T x) { /* general case */ }
template<SignedIntegral T> void process(T x) { /* for signed */ }
process(42); // SignedIntegral is more specialized → the second overload is called
process(42u); // unsigned — satisfies only Integral → the first overload
Subsumption works only if the more specific concept is defined in terms of the more general one via &&. The compiler does not compare arbitrary equivalent concepts:
// Two independent concepts with the same meaning — subsumption does not work:
template<typename T>
concept MyIntegral = std::is_integral_v<T>; // via a type trait
template<typename T>
concept AlsoIntegral = std::integral<T>; // via a standard concept
// The compiler does not know they are equivalent → ambiguous overload
Subsumption: how the compiler picks the overload
Concepts check syntax, not semantics
This is an important limitation that is easy to miss. requires { expr; } checks that the expression compiles, not that it does what is expected.
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
struct Weird {
Weird operator+(const Weird&) const {
throw std::runtime_error("does not work!"); // compiles, but...
}
};
// Weird satisfies Addable — syntactically everything is fine
// The semantics (whether the addition is meaningful) is not checked by the concept
That is why std::regular_invocable and std::invocable are syntactically identical: the standard documents a semantic difference (function purity), but the compiler does not check it. This is a deliberate design decision: semantic guarantees are the developer's responsibility.
Concepts vs type traits — what's the difference
At first glance std::integral<T> and std::is_integral_v<T> do the same thing. The difference is in overload behavior:
// Type trait — does not participate in subsumption:
template<typename T>
requires std::is_integral_v<T>
void foo(T x);
// Concept — participates in subsumption:
template<std::integral T>
void foo(T x);
The compiler understands the concept hierarchy structurally and can pick the more specialized overload automatically. With type traits this is impossible — they are opaque to the overload system.
Beyond that, concepts give readable error messages, and an IDE can display concept-annotated signatures as documentation. The standard concepts from <concepts> are the preferred choice; std::is_integral_v<T> remains useful inside if constexpr and template metaprogramming.
Practical caveats
&& in concepts and subsumption: for subsumption to work, the more specific concept must explicitly include the more general one via && in its definition. Rewrite SignedIntegral as a plain requires(...) without referencing Integral, and subsumption breaks.
Recursion and self-reference: a concept cannot reference itself directly — that is a compile error.
Concept auto in the return type: not yet supported directly. std::integral auto foo() is not valid C++20; you need an explicit template.
Concepts and explicit: concepts do not affect explicit. std::convertible_to<From, To> checks implicit conversion; std::constructible_from<To, From> checks the explicit one (via direct initialization).
Interview relevance
Concepts are gaining traction in interviews as C++20 becomes the de facto standard in new codebases. Here is what an interviewer typically checks:
What gets asked:
- How does a concept differ from SFINAE and
enable_if? (Answer: it is not just convenience — it is a different diagnostics model plus subsumption) - What is the difference between a
requires clauseand arequires expression? - What is subsumption and when does it work?
- How do concepts affect overload resolution?
- Which concepts from the standard library do you know?
Popular question directions:
- Write a concept for an arbitrary requirement (e.g. the type supports
+and<) - Explain why two semantically equivalent concepts can produce an
ambiguous overload - Walk through an abbreviated function template with
autoand explain what happens to the parameter types - Explain the limits of concepts: they check syntax, not semantics
Common wrong answer: "Concepts are just a prettier enable_if." That is wrong. Concepts are a first-class part of the C++ type system: the compiler understands them structurally (subsumption), they document intent in the signature, and they give fundamentally different diagnostics. enable_if is a hack on top of the type-deduction system; concepts are a built-in constraint mechanism.