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 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 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 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)
Infos
- »std::is_integral«: https://en.cppreference.com/w/cpp/types/is_integral
- »std::is_arithmetic«: https://en.cppreference.com/w/cpp/types/is_arithmetic
- »std::advance«: https://en.cppreference.com/w/cpp/iterator/advance





