Aus Linux-Magazin 06/2023

Die verschiedenen Arten, Concepts zu verwenden

© nsdefender / 123RF.com

Concepts erlauben es, semantische Einschränkungen auf Template-Parametern auszudrücken. Bevor man in die Details eintaucht, gilt es, zuerst ein paar Schritte zurückzutreten und die Einsatzbereiche vorzustellen.

Der vorige Artikel dieser Serie führte die Concepts ein. Im vorliegenden Beitrag geht die Reise mit den Concepts ein paar Stationen weiter. Bevor wir diese Tour antreten, gilt es jedoch, ein paar Sekunden innezuhalten und die Anwendungsfälle von Concepts noch einmal kurz und bündig zu rekapitulieren:

  • Concepts erlauben es, Anforderungen an die Template-Parameter zu formulieren,
  • erzeugen deutlich besser verständliche Fehlermeldungen,
  • können als Platzhalter für generischen Code verwendet werden,
  • lassen sich für Klassen-Templates, Funktions-Templates und Mitgliedsfunktionen von Klassen einsetzen,
  • unterstützen das Überladen von Funktionen und das Spezialisieren von Klassen-Templates, und
  • erlauben es auf einfache Weise Funktions-Templates zu definieren.

Mit diesem Hintergrund ausgestattet, können wir uns nun auf die Details konzentrieren. Zuerst einmal lassen sich Concepts auf drei Arten anwenden.

Drei Arten

Die drei Codeschnipsel aus Listing 1 verwenden das Concept »Sortable« in der Funktionsdeklaration »sort()«. Der Algorithmus »sort()« fordert, dass der Container sich sortieren lässt ist. Das Concept »Sortable« muss ein Compile-Zeit-Prädikat sein. Das bedeutet, dass der Compiler »Sortable« bereits während des Übersetzens evaluieren kann und einen Wahrheitswert zurückgibt. Die Frage, welche der drei Concepts-Arten zum Einsatz kommen soll, lässt sich recht einfach beantworten: Das hängt vom konkreten Einsatzbereich ab.

Listing 1

Drei Beispiele

### (1) Requires Clause
template<typename Cont>
requires Sortable<Cont>
void sort(Cont& container);
### (2) Trailing Requires Clause
template<typename Cont>
void sort(Cont& container) requires Sortable<Cont>;
### (3) Constrained Template Parameter
template<Sortable Cont>
void sort(Cont& container)

Klassen

Concepts lassen sich in Klassen-Templates, Member-Funktionen von Klassen und Variadic Templates einsetzen. Darüber hinaus kann man Concepts kombinieren, Funktionen auf Concepts überladen oder Klassen auf Concepts spezialisieren.

Sehen wir uns zunächst einmal Klassen an. Die Klasse »MyVector« aus Listing 2 lässt sich nur für solche Typen instanziieren, die das Concept »Object« unterstützen. Der Compiler beschwert sich in diesem Fall, dass eine Referenz kein Objekt ist.

Listing 2

Klassen

template<Object T>
class MyVector{};
MyVector<int> v1;  // OK
MyVector<int&> v2; // ERROR: int& does not satisfy the constraint Object

Nun stellt sich naturgemäß die Frage: Was ist dann ein Objekt? Darauf gibt eine mögliche Implementierung der Typ-Traits-Funktion »std::is_object« [1] die Antwort (Listing 3). Sie lautet ausformuliert: Ein Objekt ist entweder ein Skalar, ein Array, eine Union oder eine Klasse.

Listing 3

is_object

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

Member-Funktionen

Wie schon erwähnt lassen sich Concepts auch innerhalb von Member-Funktionen einer Klasse einsetzen. Ein entsprechendes Beispiel zeigt Listing 4. In diesem Fall kommt der Trailing Return Type in der Member-Funktion »push_back()« zum Einsatz. Sie fordert, dass der Template-Parameter »T« kopierbar sein muss.

Listing 4

Concepts in Member-Funktionen

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

Variadic Templates

Schließlich lassen sich Concepts auch in Variadic Templates anwenden. Ein dazu passendes Beispiel zeigt Listing 5. Die Definitionen der Funktions-Templates verwenden Fold Expressions. Die Funktionen »all()«, »any()« und »none()« verlangen von ihrem Typ-Parameter, dass er das Concept »Arithmetic« unterstützt.

Listing 5

Variadic Templates

#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 << '\n';
    std::cout << "all(5, true, 5.5, false): " << all(5, true, 5.5, false) << '\n';
    std::cout << "any(5, true, 5.5, false): " << any(5, true, 5.5, false) << '\n';
    std::cout << "none(5, true, 5.5, false): " << none(5, true, 5.5, false) << '\n';
    std::cout << std::boolalpha << '\n';
}

Arithmetic ist »T« dann, wenn es sich um eine Ganz- oder Fließkommazahl handelt. Das Concept »Arithmetic« (Zeile 4) lässt sich einfach implementieren, da es direkt den Type-Trait-Ausdruck »std::is_arithmetic« [2] verwendet. Abbildung 1 zeigt die Ausführung des Programms.

Abbildung 1: Der Einsatz von Concepts in Variadic Templates mit dem Beispielprogramm aus <a href="#artRef-l5">Listing&nbsp;5</a>.

Abbildung 1: Der Einsatz von Concepts in Variadic Templates mit dem Beispielprogramm aus Listing 5.

Mehrere Anforderungen

Meist kommt nicht nur ein Concept zum Einsatz, sondern die Kombination von mehreren. Im Beispiel aus Listing 6 fordert das Funktions-Template »find«, dass der Container »S« ein »SequenceContainer« sein muss und dass die Elemente »T« des Containers »EqualityComparable« unterstützen. Sowohl bei »SequenceContainer« als auch bei »EqualityComparable« handelt es sich um Concepts.

Listing 6

Kombination

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

Iteratoren

Die Anweisung »std::advance(iter, n)« [3] setzt den Iterator »iter« um »n« Positionen weiter. Abhängig vom Iterator kann die Implementierung Zeigerarithmetik anwenden oder lediglich »n« Schritte weitergehen. Im ersten Fall ist die Ausführungszeit konstant, im zweiten Fall hängt sie linear von der Schrittweite »n« ab.

Dank Concepts lässt sich »std::advance« aufgrund der Iterator-Kategorie überladen, wie der Code in Listing 7 zeigt. 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.

Listing 7

Iterator-Kategorie überladen

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

Spezialisierung

Schließlich unterstützen Concepts selbstverständlich auch die Template-Spezialisierung. Das soll das Beispiel aus Listing 8 deutlich machen. »MyVector<int&>« führt zum Aufruf des Klassen-Templates mit dem uneingeschränkten Template-Parameter. »MyVector<int>« hingegen führt zum Aufruf des Klassen-Templates mit dem eingeschränkten Template-Parameter.

Listing 8

Template-Spezialisierung

template<typename T>
class MyVector{};
template<Object T>
class MyVector{};
MyVector<int> v1;  // Object T
MyVector<int&> v2; // typename T

Vereinheitlichung

Zugegeben: Diese kurze und bündige Zusammenstellung der Einsatzbereiche von Concepts war wohl sehr informativ, aber auch ein wenig langweilig. Dafür wird es jetzt deutlich kurzweiliger.

Dank Concepts vereinheitlicht sich in C++20 die Syntax vereinheitlicht. Mit C++20 lässt sich an jeder Stelle ein Constrained Placeholder (Concept) verwenden, an der sich mit C++11 ein Unconstrained Placeholder (»auto«) einsetzen lässt. Listing 9 verdeutlicht das. Das ist aber noch nicht das Ende der Vereinheitlichung. Die Definition von Templates wird in C++20 zum Kinderspiel.

Listing 9

Einsatz von Concepts

#include <concepts>
#include <iostream>
#include <vector>
std::integral auto getIntegral(int val){
 return val;
}
int main(){
  std::cout << std::boolalpha << '\n';
  std::vector<int> vec{1, 2, 3, 4, 5};
  for (std::integral auto i: vec) std::cout << i << " ";
  std::integral auto b = true;
  std::cout << b << '\n';
  std::integral auto integ = getIntegral(10);
  std::cout << integ << '\n';
  auto integ1 = getIntegral(10);
  std::cout << integ1 << '\n';
  std::cout << '\n';
}

Das Concept »<std::integral>« lässt sich als Rückgabetyp (Zeile 4), in einer Range-based-For-Schleife (Zeile 10) und als Datentyp für die Variablen »b« (Zeile 11) und »integ« (Zeile 13) verwenden. Um das vordefinierte Concept »std::integral« einzusetzen, müssen Sie die Header-Datei »concept« einbinden. Die Typdeduktion mit »auto« in Zeile 15 bringt schön die Symmetrie von Concepts und »auto« auf den Punkt. Abbildung 2 zeigt die Ausgabe des Programms.

Abbildung 2: Die Ausgaben des Programms aus <a href="#artRef-l9">Listing&nbsp;9</a> demonstrieren die Einsatzbereiche von Concepts.

Abbildung 2: Die Ausgaben des Programms aus Listing 9 demonstrieren die Einsatzbereiche von Concepts.

Platzhalter

So weit, so gut. Es wird aber noch interessanter. Neben den drei am Anfang dieses Artikels vorgestellten Arten, Concepts zu verwenden, gibt es noch eine vierte: Abbreviated Function Templates.

Verwendet man bei der Deklaration einer Funktion einen Constrained Placeholder (Concept) oder einen Unconstrained Placeholder (»auto«), dann erzeugt der Compiler implizit daraus ein Funktions-Template. Listing 10 bringt alle Arten, Concepts zu verwenden, auf den Punkt. Abbildung 3 zeigt die Ausgabe des Programms.

Abbildung 3: Die Ausgaben des Programms aus <a href="#artRef-l10">Listing&nbsp;10</a> mit Constrained und Unconstrained Placeholders.

Abbildung 3: Die Ausgaben des Programms aus Listing 10 mit Constrained und Unconstrained Placeholders.

Listing 10

Placeholders

#include <concepts>
#include <iostream>
template<typename T>
requires std::integral<T>
T gcd(T a, T b) {
  if( b == 0 ) return a;
  else return gcd(b, a % b);
}
template<typename T>
T gcd1(T a, T b) requires std::integral<T> {
  if( b == 0 ){ return a; }
  else return gcd1(b, a % b);
}
template<std::integral T>
T gcd2(T a, T b) {
  if( b == 0 ){ return a; }
  else return gcd2(b, a % b);
}
std::integral auto gcd3(std::integral auto a, std::integral auto b) {
  if( b == 0 ){ return a; }
  else return gcd3(b, a % b);
}
auto gcd4(auto a, auto b) {
  if( b == 0 ){ return a; }
  else return gcd4(b, a % b);
}
int main(){
  std::cout << '\n';
  std::cout << "gcd(100, 10)= "  <<  gcd(100, 10)  << '\n';
  std::cout << "gcd1(100, 10)= " <<  gcd1(100, 10)  << '\n';
  std::cout << "gcd2(100, 10)= " <<  gcd2(100, 10)  << '\n';
  std::cout << "gcd3(100, 10)= " <<  gcd3(100, 10)  << '\n';
  std::cout << "gcd4(100, 10)= " <<  gcd4(100, 10)  << '\n';
  std::cout << '\n';
}

Die Variationen der Funktion »gcd« wenden das Concept »std::integral« auf verschiedene Weise an. »gcd« besitzt eine Requires Clause, »gcd1« die sogenannte Trailing Requires Clause und »gcd2« einen Constrained Template Parameter.

Mit »gcd3« fängt der Syntactic Sugar an. Die Funktionsdeklaration »std::integral auto gcd3(std::integral auto a, std::integral auto b)« fordert von ihren Typ-Parametern, dass diese das Concept »std::integral« unterstützen. Aus der Anwendung des Concepts entsteht ein Funktions-Template, das fast äquivalent zu den vorherigen Funktions-Templates »gcd«, »gcd1« und »gcd2« ist.

Die neue syntaktische Form von »gcd3« und »gcd4« heißt Abbreviated Function Templates. Bei »std::integral auto« in der Deklaration von »gcd3« handelt es sich um einen Constrained Placeholder (Concept). Stattdessen kann man aber auch in C++20 einen Unconstrained Placeholder (»auto«) in der Funktionsdeklaration einsetzen, wie im Falle von »gcd4«.

In den vorherigen Beschreibungen wurde der Begriff fast äquivalent verwendet. Tatsächlich sorgt das Verwenden eines Unconstrained Placeholders (»auto«) in der Funktionsdeklaration für die Erzeugung eines Funktions-Templates. Die zwei Funktionen sind daher äquivalent.

Ausblick

C++20 bringt einen reichen Satz an vordefinierten Concepts mit. Darüber hinaus lassen sich auch eigene Concepts definieren. Wie das genau funktioniert, erklärt ein Artikel in der nächsten Folge dieser Serie. (jcb/jlu)

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben