C++: Concepts, the Details
This is a cross-post from www.ModernesCpp.com.
In my last post C++20: Two Extremes and the Rescue with Concepts, I gave the first motivation for concepts. Concepts put semantic constraints on template parameters. Today, I present different use-cases for concepts in a compact form.
The Details
Just do keep it in mind: What are the advantages of concepts?
- Requirements for templates are part of the interface.
- The overloading of functions or specialisation of class templates can be based on concepts.
- We get improved error message because the compiler compares the requirements of the template parameter with the actual template arguments
- You can use predefined concepts or define your own.
- The usage of auto and concepts is unified. Instead of auto, you can use a concept.
- If a function declaration uses a concept, it automatically becomes a function template. Writing function templates is, therefore, as easy as writing a function.
This post is about the first three points. Let me show many different usages of concepts:
Three Ways
There are three ways to use the concept Sortable. For simplicity reasons, I only show the declaration of the function template.
Requires Clause
template<typename Cont> requires Sortable<Cont> void sort(Cont& container);
Trailing Requires Clause
template<typename Cont> void sort(Cont& container) requires Sortable<Cont>;
Constrained Template Parameters
template<Sortable Cont> void sort(Cont& container)
The algorithm sort requires in this case that the container is sortable. Sortable has to be a constant expression and a predicate.
Classes
You can define a class template which only accepts objects.
template<Object T> class MyVector{}; MyVector<int> v1; // OK MyVector<int&> v2; // ERROR: int& does not satisfy the constraint Object
The compiler complains that and reference is not an object. Maybe you wonder, what an object is.? A possible implementation der type-traits function std::is_object gives the answer:
template< class T> struct is_object : std::integral_constant<bool, std::is_scalar<T>::value || std::is_array<T>::value || std::is_union<T>::value || std::is_class<T>::value> {};
An object is either a scalar, or an array, or a union, or a class.
Member Functions
template<Object T> class MyVector{ ... void push_back(const T& e) requires Copyable<T>{} ... };
In this case, the member function requires that the template parameter T must be copyable.
Variadic Templates
// allAnyNone.cpp #include <iostream> #include <type_traits> template<typename T> concept Arithmetic = std::is_arithmetic<T>::value; template<Arithmetic... Args> bool all(Args... args) { return (... && args); } template<Arithmetic... Args> bool any(Args... args) { return (... || args); } template<Arithmetic... Args> bool none(Args... args) { return !(... || args); } int main(){ std::cout << std::boolalpha << std::endl; std::cout << "all(5, true, 5.5, false): " << all(5, true, 5.5, false) << std::endl; std::cout << "any(5, true, 5.5, false): " << any(5, true, 5.5, false) << std::endl; std::cout << "none(5, true, 5.5, false): " << none(5, true, 5.5, false) << std::endl; }
You can use concepts in variadic templates. The definition of the function templates are based on fold expressions. all, any, and none requires from it type parameter T that is has to support the concept Arithmetic. Arithmetic essential means that T is either integral or floating-point.
The brand-new Microsoft compiler 19.23 supports partially as the only one the proposed concepts syntax.
More Requirements
Of course, you can use more than one requirement for the template parameters.
template <SequenceContainer S, EqualityComparable<value_type<S>> T> Iterator_type<S> find(S&& seq, const T& val){ ... }
The function template find requires that the container S is a SequenceContainer and that its elements are EqualityComparable.
Overloading
std::advance(iter, n) puts its iterator iter n position further. Depending on the iterator, the implementation can use pointer arithmetic or just go n times further. In the first case, the execution time is constant; in the second case, the execution time depends on the stepsize n. Thanks to concepts, you can overload std::advance on the iterator category.
template<InputIterator I> void advance(I& iter, int n){...} template<BidirectionalIterator I> void advance(I& iter, int n){...} template<RandomAccessIterator I> void advance(I& iter, int n){...} // usage std::vector<int> vec{1, 2, 3, 4, 5, 6, 7, 8, 9}; auto vecIt = vec.begin(); std::advance(vecIt, 5); // RandomAccessIterator std::list<int> lst{1, 2, 3, 4, 5, 6, 7, 8, 9}; auto lstIt = lst.begin(); std::advance(lstIt, 5); // BidirectionalIterator std::forward_list<int> forw{1, 2, 3, 4, 5, 6, 7, 8, 9}; auto forwIt = forw.begin(); std::advance(forwIt, 5); // InputIterator
Based on the iterator category, the containers std::vector, std::list, and std::forward_list support, the best fitting std::advance implementation is used.
Specialisations
Concepts also support template specialisations.
template<typename T> class MyVector{}; template<Object T> class MyVector{}; MyVector<int> v1; // Object T MyVector<int&> v2; // typename T
- MyVector<int&> goes to the unconstrained template parameter.
- MyVector<int> goes to the constrained template parameter.
What's next?
My next post is about the syntactical unification in C++20. With C++20, you can use a constrained placeholder (concept) in each place you could use an unconstrained placeholder (auto) in C++11. But this is not the end of the unification. Defining a template becomes with C++20 a piece of cake. Just use a constrained or an unconstrained placeholder in the declaration of a function.
Passionate Developer | Leader | Mentor | Storage, Security, Networking, Telecom, Finance
4 年Nice one ??