Skip to content

Defining Concepts and Requires Clauses

C++20 introduced concepts --- named requirements for template parameters that allow the compiler To check, at the point of instantiation, whether a type satisfies a set of constraints. Concepts Make template requirements explicit, named, and composable, transforming template Metaprogramming from an implicit contract into a readable interface.

A concept is a named compile-time predicate that evaluates to true or false for a given set Of template arguments [N4950 §18.4]. The syntax is defined in [N4950 §13.9.3]:

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

A concept is declared with the concept keyword and must be defined at namespace scope. The Constraint-expression on the right-hand side is a constant expression of type bool [N4950 §13.5.3]. The concept evaluates to true if the constraint-expression is satisfied for the given Template arguments.

#include <concepts>
#include <iostream>
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
template<typename T>
concept HasSize = requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
};
template<Addable T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(3, 4) << "\n"; // OK: int is Addable
// std::cout << add("a", "b") << "\n"; // Error: const char* is not Addable
}

The concept keyword restricts what can appear on the right-hand side. Specifically [N4950 §13.5.3]:

  • The constraint-expression must be a constexpr boolean expression.
  • It cannot introduce new template parameters (those go in the template-parameter-list).
  • A concept itself is never instantiated --- it is only evaluated during constraint checking.

The Standard defines a concept as a “template that defines a set of requirements” [N4950 §18.4]. The Grammar is:

concept-definition:
template < template-parameter-list > concept attribute-specifier-seqopt identifier = constraint-expression ;

Several constraints on this grammar are important:

  1. The template-parameter-list must contain at least one type or type-constraint parameter. A concept with no parameters is ill-formed.
  2. The constraint-expression must be a constraint-expression as defined in [N4950 §13.5.3], which means it must be one of:
  • A logical AND expression of constraints (&&)
  • A logical OR expression of constraints (||)
  • A requires-expression
  • A concept name with arguments
  1. A concept is implicitly constexpr --- the compiler evaluates it at compile time. You cannot declare a concept that depends on runtime values.

  2. A concept cannot be virtual``explicit``friendOr have storage class specifiers. It is a purely compile-time entity.

  3. A concept is not a type. You cannot use it as a type argument, return type, or variable type. It can only appear in constrained contexts: template parameter lists, requires clauses, and static_assert declarations.

Proof: Concepts Are Compile-Time Boolean Predicates

Section titled “Proof: Concepts Are Compile-Time Boolean Predicates”

Claim: A concept C<T> is a compile-time predicate --- for any type T``C<T> evaluates to Either true or false during compilation, and the evaluation has no runtime side effects.

Proof:

  1. By [N4950 §13.5.3/1], a constraint-expression is defined as a logical AND/OR of atomic constraints. An atomic constraint is either a concept-id or a requires-expression.

  2. By [N4950 §13.5.3/3], an atomic constraint is formed from an expression and a mapping from template parameters to template arguments. The atomic constraint is satisfied if and only if substitution of the mapped arguments into the expression is valid and the resulting expression is true.

  3. The substitution in step 2 is performed in an unevaluated operand context [N4950 §7.5.8]. No runtime code is generated for the substitution, and no runtime side effects occur.

  4. If the substitution fails (i.e., the expression is ill-formed after substitution), the atomic constraint is not satisfied --- this is not a hard error. This is the SFINAE principle applied to constraints [N4950 §13.5.3/2].

  5. The logical combination of satisfied/unsatisfied atomic constraints via && and || produces a single boolean result. By the laws of boolean algebra, this result is well-defined and unique.

  6. Since all evaluation occurs during template argument deduction and constraint checking (which are compilation phases), the result is available at compile time with zero runtime cost.

Therefore, C<T> is a compile-time boolean predicate. \blacksquare

Corollary: A concept can be used in static_assert declarations to verify properties of types At compile time:

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
static_assert(Numeric<int>, "int must be numeric");
static_assert(Numeric<double>, "double must be numeric");
static_assert(!Numeric<std::string>, "string must not be numeric");

Corollary: Since a concept is a compile-time predicate with no runtime representation, it has Zero overhead on the generated binary. The concept is “compiled away” after constraint checking Succeeds or fails.

## Requires-Expressions

A requires-expression is the primary building block for expressing constraints on types [N4950 §7.5.8]. Its grammar is:

requires { requirement-seq }
requires parameter-list { requirement-seq }

There are four kinds of requirements that can appear in the requirement-seq [N4950 §7.5.8]:

  1. Simple requirements: an expression that must be well-formed.
  2. Type requirements: a type that must be valid (using typename keyword).
  3. Compound requirements: an expression with an expected return type.
  4. Nested requirements: a constraint that must be satisfied (using requires keyword).
#include <concepts>
#include <string>
#include <vector>
template<typename T>
concept SortableContainer = requires(T container) {
// Simple requirement: the expression must be valid
container.begin();
container.end();
container.size();
// Type requirement: the nested type must exist
typename T::value_type;
// Compound requirement: the expression must be valid AND return the specified type
{ container[0] } -> std::same_as<typename T::value_type&>;
// Nested requirement: an additional constraint must hold
requires std::random_access_iterator<decltype(container.begin())>;
};
template<SortableContainer C>
void process(C& container) {
// Guaranteed: C has begin(), end(), size(), operator[], value_type
}
int main() {
std::vector<int> v{1, 2, 3};
process(v); // OK: std::vector<int> satisfies SortableContainer
}

Each requirement in the sequence is checked independently. If any single requirement fails, the Entire requires-expression evaluates to false. Importantly, the check is performed in an unevaluated context --- no runtime code is generated for the requirement checks [N4950 §7.5.8].

Expression Validity in Requires-Expressions

Section titled “Expression Validity in Requires-Expressions”

A requires-expression tests whether an expression is well-formed, not whether it produces a Particular value. The expression a + b is valid if operator+ is accessible and unambiguous for Types a and bRegardless of the result value.

#include <iostream>
template<typename T>
concept HasStreamInsertion = requires(std::ostream& os, T value) {
{ os << value } -> std::same_as<std::ostream&>;
};
int main() {
static_assert(HasStreamInsertion<int>);
static_assert(HasStreamInsertion<const char*>);
// static_assert(HasStreamInsertion<void>); // Error: void does not satisfy
}

Substitution Failure Semantics in Requires-Expressions

Section titled “Substitution Failure Semantics in Requires-Expressions”

When the compiler evaluates a requires-expression, it performs template argument substitution for Each requirement. If substitution causes a failure (e.g., the type does not have a member function, Or an expression is ill-formed), that specific requirement is treated as unsatisfied rather than Causing a compilation error [N4950 §7.5.8/7]:

template<typename T>
concept HasFooAndBar = requires(T t) {
t.foo(); // If T doesn"t have foo(), this fails silently
t.bar(); // If T doesn't have bar(), this fails silently
// The concept evaluates to false, but NO compilation error is emitted
};

This is the SFINAE principle applied to requires-expressions. The compiler does not emit an error For a failed substitution inside a requires-expression; it records that the constraint is not Satisfied.

However, if substitution succeeds but the expression is ill-formed for a reason unrelated to Substitution (e.g., a static_assert fires inside a constexpr function called by the expression), The program is ill-formed [N4950 §7.5.8/7]:

constexpr int bad(int x) {
static_assert(x > 0, "x must be positive"); // Hard error if x <= 0
return x;
}
template<typename T>
concept Broken = requires(T t) {
bad(t); // If substitution succeeds but bad(t) fails the static_assert,
// this IS a hard error
};

A requires-clause is a mechanism for attaching a constraint to a template declaration or Function declaration [N4950 §13.9.2]. There are two syntactic forms:

Trailing requires-clause (after the function parameter list):

template<typename T>
requires std::integral<T>
T absolute(T x) {
return x < 0 ? -x : x;
}

Constrained template parameter (directly on the parameter):

template<std::integral T>
T absolute(T x) {
return x < 0 ? -x : x;
}

Both forms are semantically equivalent. The trailing requires-clause is more flexible because it can Reference multiple template parameters or the function’s own parameters [N4950 §13.9.2.1]:

#include <concepts>
#include <type_traits>
template<typename T, typename U>
requires std::integral<T> && std::integral<U>
auto safe_divide(T numerator, U denominator) -> std::common_type_t<T, U> {
return static_cast<std::common_type_t<T, U>>(numerator) / denominator;
}
// This cannot be expressed with constrained template parameters alone
// because the constraint references both T and U

A requires-clause can also appear on non-template functions (C++20) [N4950 §7.6.7]:

#include <concepts>
void process(int x)
requires (x > 0)
{
// This overload is only viable when x > 0
}

Compound Concepts: Conjunction and Disjunction

Section titled “Compound Concepts: Conjunction and Disjunction”

Constraints form a lattice ordered by subsumption [N4950 §13.5.4]. The logical operators && (conjunction) and || (disjunction) are used to combine constraints:

  • Conjunction (&&): All atomic constraints must be satisfied. This produces a constraint that is at least as strict as any individual component.
  • Disjunction (||): At least one atomic constraint must be satisfied. This produces a constraint that is at most as strict as any individual component.
#include <concepts>
#include <iostream>
#include <complex>
template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;
template<typename T>
concept Number = Arithmetic<T> || requires(T v) {
// Additional support for complex numbers, custom numeric types, etc.
{ v.real() } -> std::convertible_to<double>;
{ v.imag() } -> std::convertible_to<double>;
};
template<Number T>
T compute(T a, T b) {
return a + b;
}
int main() {
std::cout << compute(1, 2) << "\n"; // OK: int is Arithmetic
std::cout << compute(1.5, 2.5) << "\n"; // OK: double is Arithmetic
std::cout << compute(std::complex<double>(1, 0), std::complex<double>(0, 1)) << "\n";
}
## Standard Library Concepts Overview

The C++20 standard library provides a comprehensive set of concepts in the <concepts> header [N4950 §18.4]. These concepts form a hierarchy that mirrors the type categories and iterator Hierarchy of the standard library.

ConceptDescriptionKey relationships
std::same_as<T, U>T and U are the same typeReflexive, symmetric, transitive
std::derived_from<D, B>D is derived from BImplies is_base_of_v<B, D>
std::convertible_to<From, To>From is implicitly convertible to ToIncludes numeric promotions
std::integral<T>T is an integral type (excluding bool by design)Includes char``short``int``longEtc.
std::signed_integral<T>T is a signed integral typeSubsumes std::integral<T>
std::unsigned_integral<T>T is an unsigned integral typeSubsumes std::integral<T>
std::floating_point<T>T is a floating-point typefloat``double``long double
ConceptDescription
std::equality_comparable<T>== is an equivalence relation on T
std::totally_ordered<T>\<``\>``\<=``\>= define a total order on T
std::three_way_comparable<T>\<=\> is defined for T
ConceptDescription
std::copyable<T>T is copy-constructible, copy-assignable, and destructible
std::movable<T>T is move-constructible, move-assignable, and swappable
std::semiregular<T>T is copyable and default-constructible
std::regular<T>T is semiregular and equality_comparable
std::destructible<T>T can be destroyed
ConceptDescription
std::invocable<F, Args...>F can be called with Args...
std::predicate<F, Args...>F(Args...) returns something convertible to bool
std::relation<F, T, U>F(T, U) defines an equivalence relation or strict weak ordering
std::strict_weak_order<F, T, U>F(T, U) defines a strict weak ordering

The iterator concepts form a refinement hierarchy [N4950 §18.4]:

std::input_or_output_iterator<It>
└── std::input_iterator<It> (requires readable, incrementable)
└── std::forward_iterator<It> (requires multi-pass, equality comparable)
└── std::bidirectional_iterator<It> (requires decrementable)
└── std::random_access_iterator<It> (requires +=, -=, indexing)
└── std::contiguous_iterator<It> (requires contiguous memory)

Range concepts build on iterator concepts:

std::ranges::range<R> (has begin() and end())
└── std::ranges::borrowed_range<R> (safe to return by value)
└── std::ranges::sized_range<R> (has size())
└── std::ranges::view<R> (semiregular, movable, cheap to copy)
└── std::ranges::input_range<R> (input_iterator begin())
└── std::ranges::forward_range<R> (forward_iterator begin())
└── std::ranges::bidirectional_range<R>
└── std::ranges::random_access_range<R>
└── std::ranges::contiguous_range<R>

This hierarchy is designed so that subsumption works correctly: a forward_iterator subsumes an input_iteratorEnabling clean overload sets based on iterator category.

Before C++20, template constraints were expressed using SFINAE (Substitution Failure Is Not An Error) with techniques like std::enable_if``std::void_tAnd decltype. Concepts provide a Cleaner, more expressive, and more composable alternative.

AspectSFINAE (enable_if)Concepts
SyntaxVerbose, often nested in return types or default argumentsConcise, declarative
Error messagesCryptic (deep in the instantiation stack)Clear (constraint not satisfied)
ReadabilityLow (logic buried in type traits)High (named predicates)
ComposabilityManual conjunction via std::enable_if<... && ...>Natural && and || operators
SubsumptionNot supported (compiler cannot compare enable_if conditions)Supported (partial ordering on constraints)
Overload resolutionAmbiguity-prone with multiple enable_ifAmbiguity-free with proper concept design
PerformanceZero runtime overheadZero runtime overhead
Standard reference[N4950 §13.10.3] (SFINAE context)[N4950 §18.4] (concept definitions)

Example: SFINAE approach (pre-C++20)

#include <type_traits>
#include <iostream>
// Only integral types: enable_if in template parameter
template<typename T,
typename std::enable_if<std::is_integral_v<T>, int>::type = 0>
T absolute(T x) {
return x < 0 ? -x : x;
}
// Only floating-point types: enable_if in return type
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
absolute(T x) {
return x < 0 ? -x : x;
}
int main() {
std::cout << absolute(-3) << "\n"; // OK
std::cout << absolute(-3.5) << "\n"; // OK
// absolute("hello"); // Error: no matching function
}

Equivalent: Concepts approach (C++20)

#include <concepts>
#include <iostream>
template<std::integral T>
T absolute(T x) {
return x < 0 ? -x : x;
}
template<std::floating_point T>
T absolute(T x) {
return x < 0 ? -x : x;
}
int main() {
std::cout << absolute(-3) << "\n"; // OK
std::cout << absolute(-3.5) << "\n"; // OK
// absolute("hello"); // Error: constraints not satisfied (clear message)
}

The concepts version is shorter, more readable, and produces better error messages. The compiler can Also determine that std::integral<T> and std::floating_point<T> are mutually exclusive (via Subsumption), eliminating the ambiguity that would arise with two enable_if overloads that the Compiler cannot structurally compare.

The following complete example demonstrates defining a Numeric concept, using it to constrain a Template function, and providing specialized behavior for different numeric categories:

#include <concepts>
#include <iostream>
#include <iomanip>
#include <limits>
#include <type_traits>
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<typename T>
concept SignedNumeric = Numeric<T> && std::is_signed_v<T>;
template<typename T>
concept UnsignedNumeric = Numeric<T> && !SignedNumeric<T>;
template<Numeric T>
T safe_add(T a, T b) {
if constexpr (std::floating_point<T>) {
return a + b;
} else if constexpr (SignedNumeric<T>) {
if ((b > 0 && a > std::numeric_limits<T>::max() - b) ||
(b < 0 && a < std::numeric_limits<T>::min() - b)) {
throw std::overflow_error("signed overflow detected");
}
return a + b;
} else {
if (a > std::numeric_limits<T>::max() - b) {
throw std::overflow_error("unsigned overflow detected");
}
return a + b;
}
}
template<Numeric T>
auto mean(T a, T b) -> double {
return static_cast<double>(a) + static_cast<double>(b);
}
int main() {
std::cout << std::fixed << std::setprecision(1);
std::cout << "3 + 4 = " << safe_add(3, 4) << "\n";
std::cout << "1.5 + 2.5 = " << safe_add(1.5, 2.5) << "\n";
try {
safe_add(static_cast<int>(2147483647), 1);
} catch (const std::overflow_error& e) {
std::cout << "Caught: " << e.what() << "\n";
}
std::cout << "mean(3, 7) = " << mean(3, 7) << "\n";
std::cout << "mean(3.0, 7.0) = " << mean(3.0, 7.0) << "\n";
}

Output:

3 + 4 = 7
1.5 + 2.5 = 4.0
Caught: signed overflow detected
mean(3, 7) = 10.0
mean(3.0, 7.0) = 10.0