zurück zum Artikel

C++20: Concepts – die Details

Rainer Grimm

Concepts erlauben es, semantische Einschränkungen auf Template-Parametern auszudrücken. Hier nun ein Artikel mit vielen kleinen Anwendungsfällen zu Concepts.

Im letzten Artikel "C++20: Zwei Extreme und die Rettung durch Concepts [1]" bin ich auf die Motivation für Concepts eingegangen. Concepts erlauben es, semantische Einschränkungen auf Template-Parametern auszudrücken. Heute stelle ich viele kleine Anwendungsfälle zu Concepts kurz und bündig vor.

Zuerst möchte ich an die Vorteile von Concepts erinnern.

C++20: Concepts, die Details

In diesem Artikel werde ich mich mit den ersten drei Punkten der obigen Zusammenstellung genauer beschäftigen. Los geht es mit verschiedenen Anwendungen von Concepts:

Auf drei Arten lässt sich das Concept Sortable einsetzen. Ich stelle lediglich die Deklaration eines Funktions-Templates vor:

template<typename Cont>
requires Sortable<Cont>
void sort(Cont& container);
template<typename Cont>
void sort(Cont& container) requires Sortable<Cont>;
template<Sortable Cont>
void sort(Cont& container)

Der Algorithmus sort fordert, dass der Container sortierbar ist. Sortable muss ein konstanter Ausdruck und ein Prädikat sein.

Du kannst ein Klassen-Template definieren, das nur Objekte annimmt:

template<Object T>
class MyVector{};

MyVector<int> v1; // OK
MyVector<int&> v2; // ERROR: int& does not satisfy the constraint Object

Der Compiler beschwert sich in diesem Fall, dass eine Referenz kein Objekt ist. Du fragst dich vermutlich: Was ist ein Objekt? Ein mögliche Implementierung der Typ-Traits Funktion std::is_object gibt die Antwort:

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> {};

Ein Objekt ist entweder ein Skalar, ein Array, eine Union oder eine Klasse.

template<Object T>
class MyVector{
...
void push_back(const T& e) requires Copyable<T>{}
...
};

In diesem Fall fordert die Member-Funktion push_back, dass der Template-Parameter T kopierbar sein muss.

 // 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;

}

Concepts können auch in Variadic Templates angewandt werden. Die Definitionen der Funktions-Temples verwenden Fold Expressions [2]. all, any und none verlangen von ihrem Typ-Parameter, dass er das Concept Arithmetic unterstützt. Arithmetic ist T dann, wenn es eine Ganzzahl oder eine Fließkommazahl ist.

Der aktuelle Microsoft-Compiler 19.23 unterstützt als einziger teilweise die neue Syntax für Concepts:

C++20: Concepts, die Details

Natürlich lässt sich an einen Templater-Parameter mehr als eine Anforderung stellen:

template <SequenceContainer S,   
EqualityComparable<value_type<S>> T>
Iterator_type<S> find(S&& seq, const T& val){
...
}

Das Funktions-Template find fordert:

std::advance(iter, n) setzt seinen Iterator iter n Positionen weiter. Abhängig von dem Iterator kann die Implementierung Zeigerarithmetik anwenden oder lediglich n Schritte weiter gehen. Im ersten Fall ist die Ausführungszeit konstant; im zweiten Fall hängt die Ausführungszeit von der Schrittweite n ab. Dank Concepts kann std::advance aufgrund der Iterator-Kategorie überladen werden:

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

Basierend auf der Iterator-Kategorie, die die Container std::vector, std::list und std::forward_list anbieten, kommt die am besten passende Implementierung von std::advance zum Einsatz.

Concepts unterstützen natürlich auch die Template-Spezialisierung:

template<typename T>
class MyVector{};

template<Object T>
class MyVector{};

MyVector<int> v1; // Object T
MyVector<int&> v2; // typename T

In meinem nächsten Artikel schreibe ich über syntaktische Vereinheitlichung in C++20. Mit C++20 lässt sich an jeder Stelle ein constrained placeholder (Concept) verwenden, an der sich mit C++11 ein unconstrained placeholer (auto) einsetzen lässt. Dies ist aber noch nicht der Endpunkt der Vereinheitlichung. Die Definition von Templates wird in C++20 ein Kinderspiel. Verwende dazu einfach einen constrained oder einen unconstrained placeholder in der Deklaration einer Funktion. ( [3])


URL dieses Artikels:
https://www.heise.de/-4599997

Links in diesem Artikel:
[1] https://www.heise.de/blog/C-20-Zwei-Extreme-und-die-Rettung-dank-Concepts-4594834.html
[2] https://www.grimm-jaud.de/index.php/44-blog/funktional/190-fold-expressions
[3] mailto:rainer@grimm-jaud.de