C++20: Concepts – die Details
Concepts erlauben es, semantische Einschränkungen auf Template-Parametern auszudrücken. Hier nun ein Artikel mit vielen kleinen Anwendungsfällen zu Concepts.
- Rainer Grimm
Im letzten Artikel "C++20: Zwei Extreme und die Rettung durch Concepts" 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.
- Die Anforderungen an die Templates sind Bestandteil des Interfaces.
- Das Überladen von Templates oder die Spezialisierung von Templates ist aufgrund von Concepts möglich.
- Wir erhalten verbesserte Fehlermeldungen, denn der Compiler kann die Bedingungen an die Template-Parameter mit den Template-Argumenten vergleichen.
- Du kannst vordefinierte Concepts verwenden oder deine eigenen Concepts definieren.
- Die Verwendung von
auto
und Concepts wird vereinheitlicht. Daher lässt sich anstelle vonauto
einfach ein Concept verwenden.
- Wenn eine Funktionsdeklaration ein Concept verwendet, wird die Funktion automatisch zum Funktions-Template. Das Schreiben von Funktions-Templates wird damit so einfach wie das Schreiben von Funktionen.
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:
Drei Arten
Auf drei Arten lässt sich das Concept Sortable
einsetzen. Ich stelle lediglich die Deklaration eines Funktions-Templates vor:
- 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 Parameter
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.
Klassen
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.
Member-Funktionen
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.
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;
}
Concepts können auch in Variadic Templates angewandt werden. Die Definitionen der Funktions-Temples verwenden Fold Expressions. 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:
Mehrere Anforderungen
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:
- Sein Container
S
soll einSequenceContainer
sein. - Die Elemente
T
des ContainerS
sollenEqualityComparable
sein
Ăśberladen
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.
Spezialisierung
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
-
MyVector<int&>
führt zum Aufruf des Klassen-Templates mit dem uneingeschränkten Template-Parameter. MyVector<int>
führt zum Aufruf des Klassen-Templates mit dem eingeschränkten Template-Parameter.
Wie geht't weiter?
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. ()