C++20: Concepts – die Details

Modernes C++ Rainer Grimm  –  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.
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:

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:

C++20: Concepts, die Details

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.