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.
- Valid Expressions are C++ expressions that must compile successfully for the types involved in the expression to be considered models of the concept.
- Associated Types are auxiliary types that have some relation to the type T modeling the concept. The requirements in a concept typically make statements about associated types. For example, iterator requirements typically include an associated type called the value_type and the requirement that objects returned by the iterator's dereference operator must be of the value_type. In C++ it is common to use a traits class to map from the type T to the associated types of the concept.
- Invariants are run-time characteristics of types that must always be true. The invariants often take the form of preconditions and postconditions. When a precondition is not fulfilled, the behavior of the operation is, in general, undefined and can lead to a segmentation fault. This is the case for the Boost Graph Library. Some libraries provide debugging versions that use assertions or throw exceptions when a precondition is violated.
- Complexity Guarantees are maximum limits on how long the execution of one of the valid expressions will take, or how much of the various resources its computation will use.
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).
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 |
boolean-testable
|
specifies that a type can be used in Boolean contexts (exposition-only concept*) |
equality_comparable
|
specifies that operator == is an equivalence relation |
totally_ordered (Defined in header <compare> ) |
specifies that the comparison operators on the type yield a total order |
three_way_comparable
|
specifies that operator <=> produces consistent result on given types |
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 |
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.