Skip to content

Last updated:2026-10-12

C++20 Concepts

C++20 introduced concepts: a set of constraints on template parameters (evaluated at compile time). You can use them with class templates and function templates to control function overloading and specialization. Some of the benefits include:

  • Enforcing type constraints on template parameters
  • Improving code readability (replacing long SFINAE code)
  • Providing friendlier error messages
  • Preventing unintended template instantiations by restricting which types can be used

Let's start with a fairly simple addition example.

#include <iostream>

struct Foo {};

template <typename T>
T Add(T a, T b) {
  return a + b;
}

int main() {
  std::cout << Add(1, 2) << std::endl;
  Foo f1, f2;
  std::cout << Add(f1, f2) << std::endl;
  return 0;
}

Foo does not support addition, and at the same time it cannot be used with std::cout << directly, so compilation spits out a pile of errors involving operator<< and operator+. But these are not the error messages we want; what we would like is the most direct message the compiler can give us, namely: can this struct be added or not?

add.cc: In function 'int main()':
add.cc:13:13: error: no match for 'operator<<' (operand types are 'std::ostream' {aka 'std::basic_ostream<char>'} and 'Foo')
   13 |   std::cout << Add(f1, f2) << std::endl;
      |   ~~~~~~~~~ ^~ ~~~~~~~~~~~
      |        |          |
      |        |          Foo
      |        std::ostream {aka std::basic_ostream<char>}
In file included from /usr/local/Cellar/gcc/13.2.0/include/c++/13/iostream:41,
                 from add.cc:1:
/usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream:110:7: note: candidate: 'std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(__ostream_type& (*)(__ostream_type&)) [with _CharT = char; _Traits = std::char_traits<char>; __ostream_type = std::basic_ostream<char>]'
  110 |       operator<<(__ostream_type& (*__pf)(__ostream_type&))

/usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream:801:5: note:   template argument deduction/substitution failed:
/usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream: In substitution of 'template<class _Ostream, class _Tp> _Ostream&& std::operator<<(_Ostream&&, const _Tp&) [with _Ostream = std::basic_ostream<char>&; _Tp = Foo]':
add.cc:13:26:   required from here
/usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream:801:5: error: no type named 'type' in 'struct std::enable_if<false, void>'
add.cc: In instantiation of 'T Add(T, T) [with T = Foo]':
add.cc:13:19:   required from here
add.cc:7:12: error: no match for 'operator+' (operand types are 'Foo' and 'Foo')
    7 |   return a + b;
      |          ~~^~~

After implementing it with a concept:

template <typename T>
concept Addable = requires(T a, T b) { a + b; };

template <typename T>
  requires Addable<T>
T Add(T a, T b) {
  return a + b;
}

we get exactly the compile error we care about:

add_concept.cc: In function 'int main()':
add_concept.cc:17:19: error: no matching function for call to 'Add(Foo&, Foo&)'
   17 |   std::cout << Add(f1, f2) << std::endl;
      |                ~~~^~~~~~~~
add_concept.cc:10:3: note: candidate: 'template<class T>  requires  Addable<T> T Add(T, T)'
   10 | T Add(T a, T b) {
      |   ^~~
add_concept.cc:10:3: note:   template argument deduction/substitution failed:
add_concept.cc:10:3: note: constraints not satisfied
add_concept.cc: In substitution of 'template<class T>  requires  Addable<T> T Add(T, T) [with T = Foo]':
add_concept.cc:17:19:   required from here
add_concept.cc:6:9:   required for the satisfaction of 'Addable<T>' [with T = Foo]
add_concept.cc:6:19:   in requirements with 'T a', 'T b' [with T = Foo]
add_concept.cc:6:42: note: the required expression '(a + b)' is invalid
    6 | concept Addable = requires(T a, T b) { a + b; };
      |                                        ~~^~~

Now let's dive into concept syntax using the example above.

1. Concept Syntax

Syntax:

template <template-parameter-list>
concept concept-name = constraint-expression;

Mapping that onto the real example: Addable is the concept-name, and the constraint-expression is requires(T a, T b) { a + b; }.

template <typename T>
concept Addable = requires(T a, T b) { a + b; };

To use it:

#include <concepts>

The concept can be placed in several positions, as follows:

  • Where typename goes
  • After requires
  • In front of auto

1.1 Replacing typename

Constrain the template parameter by replacing typename.

// template <typename T>  typename->Addable
template <Addable T>
T Add(T a, T b) {
  return a + b;
}

1.2 The requires Keyword

We use the requires keyword in function templates. It can check whether our template T is addable; if the template can handle addition, it returns true.

requires can be placed with the template or after the function, but not after a class. Meanwhile, it comes in two forms:

  • requires with a condition

For example:

template <typename T>
  requires Addable<T>
T Add(T a, T b) { ... }
  • a requires expression

For example:

template <typename T>
concept Addable = requires(T a, T b) { a + b; };

1.2.1 Placed with the Template

Function:

template <typename T>
  requires Addable<T>
T Add(T a, T b) {
  return a + b;
}

Class:

template <typename T>
    requires Addable<T> 
class Bar {
 public:
  T Add(T a, T b) { return a + b; }
};

1.2.2 Trailing Requires Clause for Functions

Function:

template <typename T>
T Add(T a, T b)
  requires Addable<T>
{
  return a + b;
}

Classes do not support this form; it fails with: error: expected unqualified-id before 'requires' 28 | requires Addable

template <typename T>
class Bar requires Addable<T> 
{
 public:
  T Add(T a, T b) { return a + b; }
};

1.2.3 Basic requires Examples

Take the types in a database as an example. A database has various types, which we can classify as null, binary, number, and so on. Suppose we want to perform a print operation on the type being passed in, which gives us the following example:

#include <concepts>

class NumberType {};
class BaseBinaryType {};
class NullType {};
class FloatingPointType : public NumberType {};
class IntegerType : public NumberType {};
class BinaryType: public BaseBinaryType {};

template <typename T>
  requires std::is_base_of_v<NumberType, T> || std::is_base_of_v<BaseBinaryType, T>
void PrintValue(T v) {}

int main() {
  PrintValue(FloatingPointType{});
  PrintValue(NullType{});
  return 0;
}

requires lets us use ||. NullType appears in the example above; it does not satisfy the requires clause, so compilation fails with:

concept_requires.cc:16:13: error: no matching function for call to 'PrintValue(NullType)'
   16 |   PrintValue(NullType{});
      |   ~~~~~~~~~~^~~~~~~~~~~~
concept_requires.cc:12:6: note: candidate: 'template<class T>  requires (is_base_of_v<NumberType, T>) || (is_base_of_v<BaseBinaryType, T>) void PrintValue(T)'
   12 | void PrintValue(T v) {}
      |      ^~~~~~~~~~
concept_requires.cc:12:6: note:   template argument deduction/substitution failed:
concept_requires.cc:12:6: note: constraints not satisfied
concept_requires.cc: In substitution of 'template<class T>  requires (is_base_of_v<NumberType, T>) || (is_base_of_v<BaseBinaryType, T>) void PrintValue(T) [with T = NullType]':
concept_requires.cc:16:13:   required from here
concept_requires.cc:12:6:   required by the constraints of 'template<class T>  requires (is_base_of_v<NumberType, T>) || (is_base_of_v<BaseBinaryType, T>) void PrintValue(T)'
concept_requires.cc:11:45: note: no operand of the disjunction is satisfied
   11 |   requires std::is_base_of_v<NumberType, T> || std::is_base_of_v<BaseBinaryType, T>
      |            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1.3 concept and auto

Wherever auto appears, we can use it together with a concept, for example:

auto add(auto x, auto y) {
  return x + y;
}

We can change it to:

template <typename T>
concept Addable = requires(T a, T b) { a + b; };

auto add2(Addable auto x, Addable auto y) {
  return x + y;
}

At compile time we get:

concept_auto.cc:17:20: error: no matching function for call to 'add2(Foo&, Foo&)'
   17 |   std::cout << add2(f1, f2) << std::endl;
      |                ~~~~^~~~~~~~
concept_auto.cc:10:6: note: candidate: 'template<class auto:18, class auto:19>  requires (Addable<auto:18>) && (Addable<auto:19>) auto add2(auto:18, auto:19)'
   10 | auto add2(Addable auto x, Addable auto y) {
      |      ^~~~
concept_auto.cc:10:6: note:   template argument deduction/substitution failed:
concept_auto.cc:10:6: note: constraints not satisfied
concept_auto.cc: In substitution of 'template<class auto:18, class auto:19>  requires (Addable<auto:18>) && (Addable<auto:19>) auto add2(auto:18, auto:19) [with auto:18 = Foo; auto:19 = Foo]':
concept_auto.cc:17:20:   required from here
concept_auto.cc:8:9:   required for the satisfaction of 'Addable<auto:18>' [with auto:18 = Foo]
concept_auto.cc:8:19:   in requirements with 'T a', 'T b' [with T = Foo]
concept_auto.cc:8:42: note: the required expression '(a + b)' is invalid
    8 | concept Addable = requires(T a, T b) { a + b; };

2. Compiler Support

Requires GCC (10.0+) or Clang (10.0+), with the compile options -std=c++20/-std=c++2a

https://en.cppreference.com/w/cpp/compiler_support

3. Summary

With concepts provided as of C++20, we no longer need the enable_if/SFINAE machinery or function overloading to perform template constraint checks — concepts handle that for you. They offer a clearer and more powerful mechanism for constraining template parameters, making template code easier to write, understand, and maintain. By performing type checking at compile time, they help improve the robustness and readability of code.

Comments