- concepts put semantic constraints on template parameters
- requirements for template parameters are part of the interface
- concepts are compile time predicates, i.e. a function executed at compile time and returning a bool
- improved error messages (compiler detects not fulfilled requirements for given template parameters)
- there are a number of predefined concepts or you can define your own
- simplified template definition: a function using a unconstrained placeholder (auto) or a constrained placeholder (concept) automatically becomes a template
- whereever you use auto you can also use a concept
- concepts add no runtime costs
- using concepts can avoid unwanted / unexpected type conversions (e.g. bool to int) within template code
- concepts allow working with generic code at a higher level of abstraction
Syntax for using a concept
#include <concepts>
// Requires clause
template<typename T>
requires std::integral<T>
auto SomeFunc(T p)
{..}
// Trailing requires clause
template<typename T>
auto SomeFunc(T p) requires std::integral<T>
{..}
// Constrained template parameter
template<std::integral T>
auto SomeFunc(T p)
{..}
// Abbreviated function template (has no explicit "template" instruction)
auto SomeFunc(std::integral auto a)
{..}
// Concept as return type of a function
template<typename T>
requires std::integral<T>
std::integral auto SomeFunc(T p)
{..}
// Use of specialization and overloading to automatically select the most specific type
template<typename I> void DoSomething(I& iter){..}
template<std::forward_iterator I> void DoSomething(I& iter){..}
template<std::bidirectional_iterator I> void DoSomething(I& iter){..}
template<std::random_access_iterator I> void DoSomething(I& iter){..}
// Use a generic lambda to define a function template
auto Add = [](auto p1, auto p2){return p1+p2;} // unconstrained
std::integral auto Add = [](std::integral auto p1, std::integral auto p2){return p1+p2;} // constrained
// Constrained placeholders within for loop or as variable
for (std::integral auto x : vec){..}
std::integral auto myVar = 47;
Syntax for requires clause.
- named concept
requires std::integral<T>
- a combination of named concepts
- a requires expression
template <unsigned int i>
requires (i<=10)
Recommendation: do not use such compile time predicates, prefer usage of named concepts!
Predefined concepts
- std::regular
behaves like an int, e.g. a reference Type int& is not regular.
must support copy/default construction, assignment, equality, destruction, total ordering - std::copyable
e.g. not fulfilled if no copy constructor exists - std::three_way_comparable
==,!=,<,>,<=,>= - std::same_as
- std::derived_from
- std::convertible_to
- std::integral/signed_integral, unsigned_integral, floating_point
- std::default_constructible
- std::totally_ordered
Definig your own concept
template <typename T>
concept Arithmetic = std::is_integral<T>::value || std::is_floating_point<T>::value
// objects of type T can be added:
template <typename T>
concept Addable = requires (T a, T b) {a+b;}
// multiple requirements for type T
template <typename T>
concept MyTypeRequirement = requires
{
typename T::value_type; // type T has nested member value_type
typename Other<T>; // template Other can be used with type T
}
For more infos see https://mariusbancila.ro/blog/2022/06/20/requires-expressions-and-requires-clauses-in-cpp20/