C++20: Concepts – die Details

Modernes C++  –  18 Kommentare

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 von auto 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 ein SequenceContainer sein.
  • Die Elemente T des Container S sollen EqualityComparable 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.