C++ Standard Library Concepts

Concepts are an extension to C++'s templates, published as an ISO Technical Specification ISO/IEC TS 19217:2015. They are named boolean predicates on template parameters, evaluated at compile time.

A concept may be associated with a template (class template, function template, or member function of a class template), in which case it serves as a constraint: it limits the set of arguments that are accepted as template parameters.

The concept feature was postponed many times. The good news is that the C++20 will includes this interesting feature.

Some Simple Examples

You can define a concept in terms of a trait:

template <class T>
concept integral = std::is_integral_v<T>;

Or combine several traits with logic operators ||, && or !:

#include <concepts>

template <typename T>
concept IsMySupportedInt = std::same_as<T, int>
                        || std::same_as<T, long int>;

Alternatively, you can use a requires expression:

template <typename T>
concept ILabel = requires(T v)
{
  v.clear();
  {v.buildHtml()} -> std::convertible_to<std::string>;
};

(The foregoing code in curly brackets tests for member function clear() to work and for member function buildHtml to work and to return something convertible to std::string.)


Now, how are these concepts used?

An example:

template <std::integral IDX>
value_type& get(IDX idx);

NOTE: You could use just any other concept that has been defined inside or outside the STL.

Modeling Semantic Categories

The intent of concepts is to model semantic categories (Number, Range, RegularFunction) rather than syntactic restrictions (HasPlus, Array). According to ISO C++ core guideline T.20, The ability to specify a meaningful semantics is a defining characteristic of a true concept, as opposed to a syntactic constraint.

Note that in generic programming (implemented as templates in C++), we take the notion of an ADT a step further. Instead of writing down the specification for a single type, we describe a family of types that all have a common interface and semantic behavior. The set of requirements that describe the interface and semantic behavior is referred to as a concept. Algorithms constructed in the generic style are then applicable to any type that satisfies the requirements of the algorithm. This ability to use many different types with the same variable (or parameter of a function) is referred to as polymorphism.

A concept is thus a description of the requirements placed by a generic component on one or more of its arguments. A type or group of types that satisfies a concept's requirements is said to model the concept or to be a model of the concept. A concept is said to refine another concept when its requirements are a superset of those of the other concept.

Concept Requirements

Concept requirements usually come from the following categories:

Valid expressions
C++ expressions that must compile successfully for the objects involved in the expression to be considered models of the concept. For example, an Iterator x is expected to support the expressions ++x and *x.
Associated types
Types that participate in one or more of the valid expressions and that can be computed from the type(s) modeling the concept. Typically, associated types can be accessed either through typedefs nested within a class definition for the modeling type or through a traits class. For example, an iterator's value type is associated with the iterator through std::iterator_traits.
Invariants
Runtime characteristics of a model's instances that must always be true; that is, all operations on an instance must preserve these characteristics. The invariants often take the form of pre-conditions and post-conditions. For instance, after a Forward Iterator is copied, the copy and the original must compare equal.
Complexity guarantees
Maximum limits on how long the execution of one of the valid expressions will take, or how much of various resources its computation will use. Incrementing an Iterator, for example, is required to have constant complexity.

Implementation in the C++20

Class templates, function templates, and non-template functions (typically members of class templates) may be associated with a constraint, which specifies the requirements on template arguments, which can be used to select the most appropriate function overloads and template specializations.

Named sets of such requirements are called concepts. Each concept is a predicate, evaluated at compile time, and becomes a part of the interface of a template where it is used as a constraint:

#include <string>
#include <cstddef>
using namespace std::literals;

// Declaration of the concept "Hashable", which is satisfied by
// any type T such that for values a of type T,
// the expression std::hash<T>{}(a) compiles and its result is convertible to std::size_t
template<typename T>
concept bool Hashable () {
  return requires(T a) {
    { std::hash<T>{}(a) } -> std::size_t;
  };
};
<!--
template<typename T>
concept Hashable = requires(T a) {
    { std::hash<T>{}(a) } -> std::size_t;
};
-->

struct meow {};

template<Hashable T>
void f(T); // constrained C++20 function template

// Alternative ways to apply the same constraint:
// template<typename T>
//    requires Hashable<T>
// void f(T);
//
// template<typename T>
// void f(T) requires Hashable<T>;

int main() {
  f("abc"s); // OK, std::string satisfies Hashable
  f(meow{}); // Error: meow does not satisfy Hashable
}

Violations of constraints are detected at compile time, early in the template instantiation process, which leads to easy to follow error messages.

std::list<int> l = {3,-1,10};
std::sort(l.begin(), l.end());
//Typical compiler diagnostic without concepts:
//  invalid operands to binary expression ('std::_List_iterator<int>' and
//  'std::_List_iterator<int>')
//                           std::__lg(__last - __first) * 2);
//                                     ~~~~~~ ^ ~~~~~~~
// ... 50 lines of output ...
//
//Typical compiler diagnostic with concepts:
//  error: cannot call std::sort with std::_List_iterator<int>
//  note:  concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied

Syntax for Concepts

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

as in

// concept template <class T, class U> concept Derived = std::is_base_of<U, T>::value;

Concepts cannot recursively refer to themselves and cannot be constrained:

template<typename T> concept V = V<T*>; // error: recursive concept template<class T> concept C1 = true; template<C1 T> concept Error1 = true; // Error: C1 T attempts to constrain a concept definition template<class T> requires C1<T> concept Error2 = true; // Error: the requires-clause attempts to constrain a concept

Explicit instantiations, explicit specializations, or partial specializations of concepts are not allowed (the meaning of the original definition of a constraint cannot be changed).

Constraints

A constraint is a sequence of logical operations and operands that specifies requirements on template arguments. They can appear within requires-expressions (see below) and directly as bodies of concepts.

There are three types of constraints:

    1) conjunctions
    2) disjunctions
    3) atomic constraints

The constraint associated with a declaration are determined by normalizing a logical AND expression whose operands are in the following order:

    the constraint expression introduced for each constrained template parameter, in order of appearance;
    the constraint expression in the requires clause after the template parameter list;
    the constraint expression in the trailing requires clause.

This order determines the order in which constraints are instantiated when checking for satisfaction.

A constrained declaration may only be redeclared using the same syntactic form. No diagnostic is required.

template<Incrementable T>
void f(T) requires Decrementable<T>;

template<Incrementable T>
void f(T) requires Decrementable<T>; // OK, redeclaration

template<typename T>
    requires Incrementable<T> && Decrementable<T>
void f(T); // Ill-formed, no diagnostic required

// the following two declarations have different constraints:
// the first declaration has Incrementable<T> && Decrementable<T>
// the second declaration has Decrementable<T> && Incrementable<T>
// Even though they are logically equivalent.

template<Incrementable T>
void g(T) requires Decrementable<T>;

template<Decrementable T>
void g(T) requires Incrementable<T>; // ill-formed, no diagnostic required

Conjunctions

The conjunction of two constraints is formed by using the && operator in the constraint expression:

template <class T> concept Integral = std::is_integral<T>::value; template <class T> concept SignedIntegral = Integral<T> && std::is_signed<T>::value; template <class T> concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;

A conjunction of two constraints is satisfied only if both constraints are satisfied. Conjunctions are evaluated left to right and short-circuited (if the left constraint is not satisfied, template argument substitution into the right constraint is not attempted: this prevents failures due to substitution outside of immediate context).

template<typename T> constexpr bool get_value() { return T::value; } template<typename T> requires (sizeof(T) > 1 && get_value<T>()) void f(T); // #1 void f(int); // #2 void g() { f('A'); // OK, calls #2. When checking the constraints of #1, // 'sizeof(char) > 1' is not satisfied, so get_value<T>() is not checked }

Disjunctions

The disjunction of two constraints is formed by using the || operator in the constraint expression.

A disjunction of two constraints is satisfied if either constraint is satisfied. Disjunctions are evaluated left to right and short-circuited (if the left constraint is satisfied, template argument substitution into the right constraint is not attempted).

template <class T = void> requires EqualityComparable<T> || Same<T, void> struct equal_to;

Atomic Constraints

An atomic constraint consists of an expression E and a mapping from the template parameters that appear within E to template arguments involving the template parameters of the constrained entity, called its parameter mapping.

Atomic constraints are formed during constraint normalization. E is never a logical AND or logical OR expression (those form conjunctions and disjunctions, respectively).

Satisfaction of an atomic constraint is checked by substituting the parameter mapping and template arguments into the expression E. If the substitution results in an invalid type or expression, the constraint is not satisfied. Otherwise, E, after any lvalue-to-rvalue conversion, shall be a prvalue constant expression of type bool , and the constraint is satisfied if and only if it evaluates to true.

The type of E after substitution must be exactly bool. No conversion is permitted:

template<typename T> struct S { constexpr operator bool() const { return true; } }; template<typename T> requires (S<T>{}) void f(T); // #1 void f(int); // #2 void g() { f(0); // error: S<int>{} does not have type bool when checking #1, // even though #2 is a better match }

Two atomic constraints are considered identical if they are formed from the same expression at the source level and their parameter mappings are equivalent.

template<class T> constexpr bool is_meowable = true;
template<class T> constexpr bool is_cat = true;

template<class T>
concept Meowable = is_meowable<T>;

template<class T>
concept BadMeowableCat = is_meowable<T> && is_cat<T>;

template<class T>
concept GoodMeowableCat = Meowable<T> && is_cat<T>;

template<Meowable T>
void f1(T); // #1

template<BadMeowableCat T>
void f1(T); // #2

template<Meowable T>
void f2(T); // #3

template<GoodMeowableCat T>
void f2(T); // #4

void g(){
    f1(0); // error, ambiguous:
           // the is_meowable<T> in Meowable and BadMeowableCat forms distinct
           // atomic constraints that are not identical (and so do not subsume each other)

    f2(0); // OK, calls #4, more constrained than #3
           // GoodMeowableCat got its is_meowable<T> from Meowable
}

Conceptual Template Parameters

Concepts are template parameters that do not define types that are used by the class, but control the behaviour of the class through defining which abstract data type it implements.

One of the primary responsibilities of a generic library is to define the interfaces that allow algorithms to be written independently of any particular data structure. Note that by interface we do not merely mean a set of function prototypes. Instead, we mean a set of syntactic requirements—things like function names and numbers of arguments—as well as semantic requirements (executing the function must have certain effects) and time and space complexity guarantees.

Using the terminology from the book Generic Programming and the STL, we use the word concept to refer to this richer notion of an interface. The STL defines a collection of iterator concepts that provide a generic mechanism for traversing and accessing sequences of objects.

The authors of the Boost library strongly recommend that:

Every programmer writing class or function templates ought to make concept checking a normal part of their code writing routine.

Indeed, Boost includes a library of classes for checking concepts and a mechanism for defining new concept-checking classes.

Here is an example of a class defined solely to be used as a template parameter for checking purposes:

class integer_class {}; template <typename I, class IS_INTEGER> class integer {};

Next, a partial specialization is added for the case when the second template parameter is class integer_class. This becomes the only implementation of the class, whereas the general class is made unusable through having its constructors deleted or some other means.

template <typename I> class integer<I,integer_class> {/* code */};

...

Sets of Requirements

The requirements for a concept consist of a set of valid expressions, associated types, invariants, and complexity guarantees. A type that meets the set of requirements is said to model the concept. A concept can extend the requirements of another concept, which is called refinement.

Example: InputIterator

In this section, we take a closer look at InputIterator as an example of a concept. First, the InputIterator concept is a refinement of TrivialIterator which, in turn, is a refinement of Assignable and EqualityComparable . Thus, the InputIterator meets all the requirements of a TrivialIterator (which meets all of the requirements of Assignable and EqualityComparable) The result is that a type that models InputIterator will have a dereference operator, it can be copied and assigned, and it can be compared with other iterator objects using == and != .

The InputIterator concept adds the requirement for pre-increment and post-increment operators. These requirements are denoted by the following valid expressions. Objects i and j are instances of a type T that models InputIterator.

i = j // assignment (from Assignable) T i(j) // copy (from Assignable) i == j // equality test (from EqualityComparable) i != j // inequality test (from EqualityComparable) *i // dereference (from TrivialIterator) ++i // pre-increment operator i++ // post-increment operator

The std::iterator_traits class provides access to the associated types of an iterator type. The type of an object that is pointed to by an iterator type (call it X) can be determined via the value_type of the traits class. The other associated types are reference, pointer, difference_type, and iterator_category.

In the following function template we show the use of the iterator_traits class to obtain the value_type of the iterator and dereference the iterator.

template <typename Iterator> void dereference example(Iterator i) { typename iterator traits<Iterator>::value type t; t = *i; }

As for complexity guarantees, all of the InputIterator operations are required to be constant time. Some types that model InputIterator are std::list<int>::iterator, double*, and std::istream iterator<char>.

The purpose of defining concepts becomes clear when considering the implementation of generic algorithms. The implementation of the std::for_each() function follows. Inside the function precisely four operations are applied to the iterator objects first or last : comparison using operator!=(), increment with operator++(), dereference with operator*(), and copy construction. For this function to compile and operate properly the iterator arguments must support at least these four operations. The concept InputIterator includes these operations (and not many more), so it is a reasonable choice for succinctly describing the requirements of for_each().

template <typename InputIterator, typename Function>
Function for_each(InputIterator first, InputIterator last, Function f ) {
  for ( ; first != last; ++first)
  f(*first);
  return f ;
}

Standard Concepts

The concepts library provides definitions of fundamental library concepts that can be used to perform compile-time validation of template arguments and perform function dispatch based on properties of types.

Most concepts in the standard library impose both syntactic and semantic requirements. It is said that a standard concept is satisfied if its syntactic requirements are met, and is modeled if it is satisfied and its semantic requirements (if any) are also met.


All the concepts below demand inclusion of <concepts> (C++20).

Core language concepts
same_as specifies that a type is the same as another type
derived_from specifies that a type is derived from another type
convertible_to specifies that a type is implicitly convertible to another type
common_reference_with specifies that two types share a common reference type
common_with specifies that two types share a common type
integral specifies that a type is an integral type
signed_integral specifies that a type is an integral type that is signed
unsigned_integral specifies that a type is an integral type that is unsigned
floating_point specifies that a type is a floating-point type
assignable_from specifies that a type is assignable from another type
swappable/swappable_with specifies that a type can be swapped or that two types can be swapped with each other
destructible specifies that an object of the type can be destroyed
constructible_from specifies that a variable of the type can be constructed from or bound to a set of argument types
default_initializable specifies that an object of a type can be default constructed
move_constructible specifies that an object of a type can be move constructed
copy_constructible specifies that an object of a type can be copy constructed and move constructed
Comparison concepts
boolean-testable specifies that a type can be used in Boolean contexts (exposition-only concept*)
equality_comparable
equality_comparable_with
specifies that operator == is an equivalence relation
totally_ordered
totally_ordered_with
(Defined in header <compare>)
specifies that the comparison operators on the type yield a total order
three_way_comparable
three_way_comparable_with
specifies that operator <=> produces consistent result on given types
Object concepts
movable specifies that an object of a type can be moved and swapped
copyable specifies that an object of a type can be copied, moved, and swapped
semiregular specifies that an object of a type can be copied, moved, swapped, and default constructed
regular specifies that a type is regular, that is, it is both semiregular and equality_comparable
Callable concepts
invocable/regular_invocable specifies that a callable type can be invoked with a given set of argument types
predicate specifies that a callable type is a Boolean predicate
relation specifies that a callable type is a binary relation
equivalence_relation specifies that a relation imposes an equivalence relation
strict_weak_order specifies that a relation imposes a strict weak ordering

Additional concepts can be found in the iterators library, the algorithms library, and ranges library.