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
  1. 1What a concept is
  2. 2Four ways to constrain a template
  3. 3Writing a concept with a requires expression
  4. 4Use the standard library’s concepts first
  5. 5Overloading on constraints: the more constrained template wins
  6. 6Replacing enable_if and SFINAE
  7. 7What concepts don’t check
  8. 8How concepts reached C++20
  9. 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 shape-sorter box in isometric view: a violet round peg dropping into the one round hole, and a square peg too wide for it resting on the lid
Figure 1 — A constraint is checked at the call, before the template body is compiled.

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::integral T> T half(T x) 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 std::enable_if with a concept, template <std::integral T>.

since C++20

What a concept is

A concept is a template that names a constraint, a compile-time bool expression over its parameters:

[Example 1:
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

[Example 2:
// 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:

[Example 3:
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::integral<double>' evaluated to false. Pick the form that reads best: a type-constraint when each parameter needs one concept, a requires clause for a combination (requires std::integral<T> && (sizeof(T) >= 4)), 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.

[Example 4:
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::integral auto x), 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:

[Example 5:
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:

[Example 6:
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::value_type; asks that the type exists.
  • Compound: { e } -> C; asks that e compiles and that decltype((e)), passed as the concept’s first argument, satisfies C. -> std::same_as<bool> therefore demands exactly a bool prvalue, while -> std::convertible_to<std::size_t> accepts any type that converts. A noexcept after the braces also requires that e can’t throw.
  • Nested: requires std::default_initializable<C>; asks that a further constraint is satisfied, not merely that it compiles.

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:

[Example 7:
// 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.
[Example 8:
// 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:

[Example 9:
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:

[Example 10:
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::random_access_iterator is defined in terms of std::bidirectional_iterator, which is defined in terms of std::forward_iterator 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:

[Example 11:
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:

[Example 12:
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::signed_integral<T> is defined as std::integral<T> && std::is_signed_v<T>, so it subsumes std::integral<T>:

[Example 13:
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:

[Example 14:
// 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:

[Example 15:
// 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:

[Example 16:
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:

[Example 17:
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::equality_comparable, 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::equality_comparable, 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:

[Example 18:
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:

[Example 19:
#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::is_trivially_destructible_v<T> 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::equality_comparable 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.

Read next

  1. C++ expression templates explained with a C++20 exampleTemplates · Miriam Castellanos · written for C++20
  2. C++ generic lambdas, template lambdas and function objectsTemplates · Miriam Castellanos · written for C++20
  3. std::variant in C++: std::visit, std::get and the trapsStandard Library · Miriam Castellanos · written for C++17