Standards[std]
C++20 concepts: how to write and use them
- Section
- Standards [std]
- Standard
- C++20
- Compilers
- MSVC 19.44, Clang 19.1
- Published
- Length
- 13 min read
In this answer9 clauses
- 1What a concept is
- 2Four ways to constrain a template
- 3Writing a concept with a requires expression
- 4Use the standard library’s concepts first
- 5Overloading on constraints: the more constrained template wins
- 6Replacing enable_if and SFINAE
- 7What concepts don’t check
- 8How concepts reached C++20
- 9Compiler support and the feature-test macro
Abstract
A C++20 concept is a named compile-time predicate on template arguments: write one with a requires expression, put it where typename would go, in a requires clause or before auto, and calls with the wrong types fail at the call with the concept named. It replaces most enable_if and SFINAE code, but it checks only that expressions compile, not what the body uses or what the operations mean.

A concept is a named compile-time predicate on template arguments, and a template that uses one accepts only arguments that satisfy it: template <std:: can’t be called with a double, and the compiler says so at the call, naming std::integral. Reach for the standard library’s concepts first (<concepts>, <iterator>, <ranges>), write your own with a requires expression when none fits, and use them where you would have written std::enable_if.
This guide is written for C++20, the standard that added concepts. Before it, the same effect took std::enable_if and SFINAE (C++11) or the detection idiom with std::void_t (C++17), and a wrong argument type was reported from deep inside the template, or as a list of candidates that didn’t match. Every sample was compiled with MSVC 19.44 (/std:c++20 /EHsc /W4 /permissive-) and Clang 19.1 (-std=c++20 -Wall -Wextra, on Microsoft’s standard library) on one Windows x64 machine, without optimization unless the text says otherwise; the error messages and macro values quoted are what those builds produced there.
What changed: a template is constrained with with a concept, std::enable_iftemplate <std::.
since C++20
What a concept is
A concept is a template that names a constraint, a compile-time bool expression over its parameters:
template <class T>
concept Hashable = requires(const T& x) {
{ std::hash<T>{}(x) } -> std::convertible_to<std::size_t>;
};
static_assert(Hashable<int>);
static_assert(Hashable<std::string>);
static_assert(!Hashable<std::vector<int>>); // no std::hash for it
template <class T>
void describe(const T&) {
if constexpr (Hashable<T>)
std::puts("can be a key in std::unordered_set");
else
std::puts("needs a hash function first");
}— end example]
Hashable<int> is a constant true or false, so it works in static_assert and if constexpr as well as in a template’s declaration. A concept is defined at namespace scope, and it can’t refer to itself, be constrained, or be specialized (cppreference, Constraints and concepts). When a template’s declaration names a concept, overload resolution checks it at the call: a candidate whose constraints aren’t satisfied is dropped, as SFINAE drops one whose signature fails to substitute, but the reason has a name.
Four ways to constrain a template
// 1. A type-constraint where class or typename would go
template <std::integral T>
T half1(T x) { return x / 2; }
// 2. A requires clause after the template parameter list
template <class T>
requires std::integral<T>
T half2(T x) { return x / 2; }
// 3. A trailing requires clause after the function declarator
template <class T>
T half3(T x) requires std::integral<T> { return x / 2; }
// 4. Constrained auto: an abbreviated function template
auto half4(std::integral auto x) { return x / 2; }— end example]
All four reject a double. For half1(2.5), Clang 19 says:
error: no matching function for call to 'half1'
note: candidate template ignored: constraints not satisfied [with T = double]
note: because 'double' does not satisfy 'integral'— end example]
MSVC 19.44 reports that the concept 'std::. Pick the form that reads best: a type-constraint when each parameter needs one concept, a requires clause for a combination (requires std::), a trailing clause when the condition reads better after the signature or uses the function’s parameters, and constrained auto for short declarations. Constrained auto has one difference: every auto is a template parameter of its own, so two constrained auto parameters can have two types.
template <std::integral T>
T add(T a, T b) { return a + b; } // a and b: one type
auto add4(std::integral auto a, std::integral auto b) {
return a + b; // two types: int and long
}— end example]
add(1, 2L) fails deduction; add4(1, 2L) returns a long. Constrained auto works in lambda parameters too, [](std::, and C++ generic lambdas, template lambdas and function objects shows when a template lambda is the better way to tie two parameters together.
The trailing form also constrains a member of a class template that isn’t a template itself:
template <class T>
struct Box {
T value;
T twice() const requires std::integral<T> { return value * 2; }
};
Box<int> b{21}; // b.twice() == 42
Box<std::string> s{"no twice() here"}; // fine until you call it— end example]
Box<std::string> still compiles. Calling s.twice() doesn’t: Clang 19 reports “invalid reference to function ‘twice’: constraints not satisfied”.
Writing a concept with a requires expression
A requires expression lists what must be valid for its parameters, and is true when all of it is:
template <class C>
concept Container = requires(C& c, const C& cc) {
typename C::value_type; // type
c.begin(); // simple
c.end();
{ cc.size() } -> std::convertible_to<std::size_t>; // compound
{ cc.empty() } noexcept -> std::same_as<bool>; // compound
requires std::default_initializable<C>; // nested
};
static_assert(Container<std::vector<int>>);
static_assert(Container<std::string>);
static_assert(!Container<int[4]>); // no value_type, no .size()— end example]
The four kinds of requirement (cppreference’s page on requires expressions has the full rules):
- Simple:
c.begin();asks only that the expression compiles. It is never evaluated. - Type:
typename C::asks that the type exists.value_type; - Compound:
{ e } -> C;asks thatecompiles and thatdecltype((e)), passed as the concept’s first argument, satisfiesC.-> std::therefore demands exactly asame_as<bool> boolprvalue, while-> std::accepts any type that converts. Aconvertible_to<std:: size_t> noexceptafter the braces also requires thatecan’t throw. - Nested:
requires std::asks that a further constraint is satisfied, not merely that it compiles.default_initializable<C>;
Inside a template, a requirement that fails makes the whole expression false; it isn’t an error. Outside a template it is: requires { x.reserve(1); } with an int x in main fails to compile on both compilers here. Two more forms are worth knowing:
// An ad hoc constraint: requires requires. Prefer naming a concept.
template <class T>
requires requires(T a, T b) { a + b; }
T sum(T a, T b) { return a + b; }
// A requires expression as a compile-time test inside a template.
template <class C>
void reserve_if_possible(C& c, std::size_t n) {
if constexpr (requires { c.reserve(n); })
c.reserve(n);
}— end example]
requires requires is not a typo: the first requires opens a clause and the second a requires expression. It works, but a named concept reads better and can be reused, and only constraints that come from the same concept can subsume one another, as the overloading section below shows. reserve_if_possible calls reserve on a std::vector and skips it for a std::list.
Use the standard library’s concepts first
The C++ Core Guidelines put it as rule T.11, “Whenever possible use standard concepts”. The standard writes down their semantic requirements, and the rest of the library is constrained with them, so your templates and the library’s agree on what a range or an iterator is. The ones you’ll reach for most:
<concepts>:same_as,derived_from,convertible_to,integral,floating_point,constructible_from,default_initializable,movable,copyable,regular,equality_comparable,totally_ordered,invocable,predicate,strict_weak_order.<compare>:three_way_comparable.<iterator>:input_iterator,forward_iterator,bidirectional_iterator,random_access_iterator,contiguous_iterator,sortable.<ranges>:range,input_range,sized_range,random_access_range,contiguous_range,view.
// The mean of any range of floating-point values.
template <std::ranges::input_range R>
requires std::floating_point<std::ranges::range_value_t<R>>
auto mean(R&& r) {
std::ranges::range_value_t<R> sum{};
std::size_t n = 0;
for (auto x : r) {
sum += x;
++n;
}
return n ? sum / n : sum;
}
// Call f on each element; f must accept what the range yields.
template <std::ranges::input_range R,
std::invocable<std::ranges::range_reference_t<R>> F>
void for_each_item(R&& r, F f) {
for (auto&& x : r) std::invoke(f, x);
}— end example]
mean takes a std::vector<double>, a std::list<float> or a view, and rejects a range of int. The standard algorithms in std::ranges are constrained the same way, which is what their error messages show. std::ranges::sort on a std::list<int> gets, from Clang 19, a chain of notes that ends in the reason:
note: because 'std::list<int> &' does not satisfy 'random_access_range'— end example]
Overloading on constraints: the more constrained template wins
When two constrained templates both accept a call, the one whose constraints subsume the other’s is chosen. The iterator concepts are built on each other, so overloads on them sort themselves out:
template <std::input_iterator It>
const char* kind(It) { return "input"; }
template <std::bidirectional_iterator It>
const char* kind(It) { return "bidirectional"; }
template <std::random_access_iterator It>
const char* kind(It) { return "random access"; }
// kind(vec.begin()): "random access"; kind(lst.begin()):
// "bidirectional"; kind(std::istream_iterator<int>(in)): "input"— end example]
A std::vector iterator satisfies all three. std:: is defined in terms of std::, which is defined in terms of std:: and so on down, so the random access constraints contain the others and that overload wins. Before C++20 this took tag dispatch on iterator_category.
Subsumption compares constraints by where they came from, not by what they say. The standard calls two atomic constraints identical only when they are formed from the same appearance of the same expression in the source ([temp.constr.atomic]), so the same trait written twice doesn’t count:
template <class T>
requires std::is_integral_v<T>
int f(T) { return 1; }
template <class T>
requires std::is_integral_v<T> && std::is_signed_v<T>
int f(T) { return 2; }
// f(1); // error: ambiguous— end example]
MSVC 19.44 reports an “ambiguous call to overloaded function”. Clang 19 adds the reason:
note: similar constraint expressions not considered equivalent; constraint
expressions cannot be considered equivalent unless they originate from
the same concept— end example]
The fix is to build on concepts. std:: is defined as std::, so it subsumes std::integral<T>:
template <std::integral T>
int f(T) { return 1; }
template <std::signed_integral T> // integral<T> && is_signed_v<T>
int f(T) { return 2; }
// f(1) == 2, f(1u) == 1— end example]
Replacing enable_if and SFINAE
Most C++17 constraint code has a direct C++20 form. Here are the three common patterns before:
// C++17: two overloads kept apart by enable_if
template <class T,
std::enable_if_t<std::is_integral_v<T>, int> = 0>
const char* describe(T) { return "integer"; }
template <class T,
std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
const char* describe(T) { return "floating point"; }
// C++17: a member that exists only for some T
template <class T>
struct Box17 {
T value;
template <class U = T,
std::enable_if_t<std::is_integral_v<U>, int> = 0>
U twice() const { return value * 2; }
};
// C++17: the detection idiom
template <class T, class = void>
struct has_reserve : std::false_type {};
template <class T>
struct has_reserve<T, std::void_t<decltype(std::declval<T&>().reserve(0))>>
: std::true_type {};— end example]
And after:
// C++20: the same overloads, constrained
const char* describe(std::integral auto) { return "integer"; }
const char* describe(std::floating_point auto) {
return "floating point";
}
// C++20: an ordinary member with a trailing requires clause
template <class T>
struct Box {
T value;
T twice() const requires std::integral<T> { return value * 2; }
};
// C++20: a concept instead of the detection idiom
template <class T>
concept HasReserve = requires(T& t) { t.reserve(0); };— end example]
The C++17 member had to become a template with a dummy parameter U, only so that SFINAE had something to substitute into; the C++20 one stays an ordinary function. Constraints also work on non-type template parameters:
template <std::size_t N>
requires (N > 0)
struct NonEmpty {
char data[N];
};
// NonEmpty<0> no; // error: constraints not satisfied— end example]
A compile-time string type can put limits on its length with the same kind of clause; C++ string as a template parameter builds such a type, fixed_string, and passes it as a class-type template parameter. Operator templates are the other common case: C++ expression templates explained constrains its operators with a concept so that they don’t match unrelated types.
Keep enable_if where code must still compile as C++17. For the rest, clang-tidy’s modernize-use-constraints check finds enable_if in a return type or in a trailing template parameter and rewrites it as a requires clause; according to its documentation, other uses and Boost’s enable_if are left alone. clang-tidy is on Toolchain.
What concepts don’t check
A constraint is checked against the template’s arguments at the call. The body is compiled when the template is instantiated, as it always was, and nothing compares what the body uses with what the constraint promised:
template <std::equality_comparable T>
bool in_order(const T& a, const T& b) {
return !(b < a); // uses <, which the constraint never asked for
}
struct Id {
int value;
bool operator==(const Id&) const = default;
};
bool ok = in_order(1, 2); // int has == and <: fine
// in_order(Id{1}, Id{2}); // passes the constraint, fails in the body— end example]
Id satisfies std::, so the call passes overload resolution, and the error comes from inside the body. Clang 19 reports “invalid operands to binary expression (‘const Id’ and ‘const Id’)” at b < a, followed by in instantiation of function template specialization 'in_order<Id>' requested here. The fix is to constrain on what the body uses: with std::totally_ordered in place of std::, the same call fails at the call, “because ‘Id’ does not satisfy ‘totally_ordered’”.
The second gap is meaning. cppreference’s page on the concepts library separates a concept being satisfied (its syntactic requirements are met) from being modeled (its semantic requirements are met too), and says that in general only the syntactic requirements can be checked by the compiler. A program that depends on a concept being modeled by a type that only satisfies it is ill-formed, no diagnostic required:
struct Approx {
double v;
bool operator==(const Approx& o) const {
return std::abs(v - o.v) < 0.1; // not transitive
}
};
// Satisfied: == compiles and returns bool. Not modeled: == is not
// an equivalence relation, and no compiler can tell.
static_assert(std::equality_comparable<Approx>);
// Approx{0.0} == Approx{0.08} and Approx{0.08} == Approx{0.16},
// but Approx{0.0} != Approx{0.16}— end example]
The static_assert passes on both compilers. A concept documents what a template means to require; whether a type keeps the promise is still the type author’s job.
How concepts reached C++20
Concepts were in the draft of C++0x, the standard that became C++11, until the committee’s meeting in Frankfurt in July 2009 took them out. Herb Sutter’s trip report, written a week after the meeting, gives “normal project management considerations” as the reason: the design was still being worked out, a formal poll put the expected time to finish it at between three and four years, and the committee chose to ship C++0x sooner and continue concepts for a future standard.
The work went on as a Technical Specification, which ISO published in November 2015 as ISO/IEC TS 19217:2015, C++ Extensions for concepts (withdrawn in 2022). At its July 2017 meeting in Toronto the committee merged the TS into the C++20 draft, except, for the time being, its “introducer syntax” and “terse/natural syntax”, as Sutter’s report from Toronto records. The std::integral auto form in today’s standard came from a later paper, which is why the vendors, and Since When, give it a row of its own.
Compiler support and the feature-test macro
| Feature | GCC | Clang | MSVC |
|---|---|---|---|
| Concepts (P0734R0) | 10 | 10 | VS 2019 16.3 |
Abbreviated function templates (constrained auto) |
10 | 10 | VS 2019 16.9 |
These are the first releases each vendor lists, from the C++20 table of Since When. Turn on C++20 with -std=c++20 for GCC and Clang, and /std:c++20 for MSVC from Visual Studio 2019 16.11 (/std:c++latest before it); GCC builds as C++20 by default since GCC 16. Follow-up papers, such as the one for conditionally trivial special member functions (P0848R3), arrived in their own releases; check them on your compiler’s status page.
To test for concepts in code, use the feature-test macros. __cpp_concepts is predefined by the compiler: 201907L means constraints, and 202002L adds conditionally trivial special member functions. __cpp_lib_concepts, from <version> or <concepts>, says the library’s concepts exist:
#if __has_include(<version>) // a C++20 header; Microsoft's STL also
#include <version> // has it in C++14 and C++17 modes
#endif
#if defined(__cpp_lib_concepts) // <concepts> and std::integral exist
#include <concepts>
template <std::integral T>
T half(T x) { return x / 2; }
#else
#include <type_traits>
template <class T,
std::enable_if_t<std::is_integral<T>::value, int> = 0>
T half(T x) { return x / 2; }
#endif— end example]
With /std:c++20 and -std=c++20, both compilers here give __cpp_concepts and __cpp_lib_concepts the value 202002; in C++23 mode __cpp_lib_concepts becomes 202207L. Don’t test __cplusplus instead: MSVC 19.44 reports 199711L even under /std:c++20 unless you add /Zc:__cplusplus.
The 202002 value is what lets a class choose between special members by constraint, which enable_if never could, since a destructor can’t be a template: a wrapper declares a defaulted destructor constrained on std:: beside a hand-written one, and stays trivially destructible for trivial types. The rule of zero guide builds such a wrapper and checks it on both compilers. More on what each standard changed, and when your compilers caught up, is in Standards.
Questions
Do concepts replace enable_if?
In code compiled as C++20, almost always. A type-constraint or requires clause says the same thing more directly, overloads are ordered by subsumption instead of hand-written exclusive conditions, and errors name the concept. Keep enable_if where the code must still compile as C++17 or earlier; clang-tidy’s modernize-use-constraints check rewrites the two most common enable_if patterns into requires clauses.
What is a requires expression?
A compile-time bool expression, requires (parameters) { requirements; }, that is true when every requirement inside holds. A requirement can ask that an expression compiles, that a type exists, that an expression has a type satisfying a concept (optionally noexcept), or that a further constraint holds. Inside a template, a requirement that fails makes the expression false instead of causing an error, which is why most concepts are written with one.
Why were concepts left out of C++11?
At the committee’s meeting in Frankfurt in July 2009 they were taken out of the C++0x draft, the standard that became C++11. Herb Sutter’s trip report gives the reason as schedule: the design still needed work, a formal poll put the time to finish it at three to four years, and the committee chose to ship C++0x sooner and continue concepts for a later standard.
Do concepts check the body of a template?
No. A constraint is checked against the arguments at the call; the body is compiled when the template is instantiated, and nothing compares what it uses with what the constraint promised. A template constrained on std:: whose body uses < compiles until it is called with a type that has == but no <.
Which compilers support C++20 concepts?
GCC 10, Clang 10 and Visual Studio 2019 16.3 are the first releases their vendors list for P0734R0, the paper that added concepts; the constrained auto form came in GCC 10, Clang 10 and Visual Studio 2019 16.9. Turn on C++20 with -std=c++20 or /std:c++20. The C++20 table of Since When has both rows, with the vendor pages they come from.
Sources
- Constraints and concepts (since C++20)cppreference.com
- Requires expression (since C++20)cppreference.com
- Concepts library (since C++20)cppreference.com
- Trip Report: Exit Concepts, Final ISO C++ Draft in ~18 MonthsSutter’s Mill,