Definition von Concepts

(Bild: Shutterstock.com/Kenishirotie)
Es gibt zwei Möglichkeiten, ein Concept zu definieren: Bestehende Concepts und Compile-Zeit-Prädikate lassen sich kombinieren oder Requires Expression anwenden.
Bevor ich über C++20 und Concepte schreibe, möchte ich eine kurze Anmerkung machen.
Meine zweite Iteration durch C++20
Ich habe bereits mehr als 80 Artikel über C++20 [1] und 20 über Concepts [2] geschrieben. Meine früheren C++20-Artikel sind ein bis drei Jahre alt. Das hat zwei wichtige Auswirkungen: Erstens habe ich in der Zwischenzeit eine Menge neuer Dinge über C++20 gelernt. Zweitens haben viele Leser meine früheren Artikel vermutlich nicht im Kopf. Folglich biete ich in diesem Artikel in meiner zweiten Iteration durch C++20 so viel Inhalt, dass alle meinen Erklärungen folgen können und bei Bedarf Links zu meinen früheren Artikeln finden.

Dieser Strategie folgend, ist hier die allgemeine Idee der Concepts:
Concepts
Die generische Programmierung mit Templates ermöglicht es, Funktionen und Klassen zu definieren, die mit verschiedenen Typen verwendet werden können. Daher ist es nicht ungewöhnlich, dass man ein Template mit dem falschen Typ instanziiert. Das Ergebnis können viele Seiten mit kryptischen Fehlermeldungen sein. Dieses Problem wird mit Concepts gelöst. Concepts ermöglichen es, Anforderungen für Template-Parameter zu schreiben, die vom Compiler überprüft werden, und revolutionieren die Art und Weise, wie wir über generischen Code nachdenken und ihn schreiben. Hier ist der Grund dafür:
- Anforderungen für Template-Parameter werden Teil ihrer öffentlichen Schnittstelle.
- Das Überladen von Funktionen oder Spezialisierungen von Klassen-Templates kann auf Concepts basieren.
- Wir erhalten verbesserte Fehlermeldungen, weil der Compiler die definierten Anforderungen für Template-Parameter mit den gegebenen Template-Argumenten abgleicht.
Das ist aber noch nicht das Ende der Fahnenstange:
- Man kann vordefinierte Concepts verwenden oder eigene definieren.
- Die Verwendung von
auto
und Concepts ist vereinheitlicht. Anstelle vonauto
lässt sich ein Concept verwenden. - Wenn eine Funktionsdeklaration ein Concept verwendet, wird sie automatisch zu einem Funktions-Template. Das Schreiben von Funktions-Templaten ist daher so einfach wie das Schreiben einer Funktion.
Der folgende Codeschnipsel veranschaulicht die Definition und die Verwendung des einfachen Concepts Integral:
template <typename T>
concept Integral = std::is_integral<T>::value;
Integral auto gcd(Integral auto a, Integral auto b) {
if( b == 0 ) return a;
else return gcd(b, a % b);
}
Das Integral
-Concept verlangt von seinem Typ-Parameter T
, dass std::is_integral<T>::value
den Wert true
ergibt. std::is_integral<T>::value
ist eine Funktion aus der Type Traits Library [3], die zur Compile-Zeit überprüft, ob T
integral ist. Wenn std::is_integral<T>::value
den Wert true ergibt, ist alles in Ordnung; andernfalls bekommt man einen Compile-Zeit-Fehler.
Der gcd
-Algorithmus bestimmt den größten gemeinsamen Teiler zweier Zahlen auf der Grundlage des euklidischen Algorithmus. Der Code verwendet die sogenannte abgekürzte Abreviated Funtion Template Syntax, um gcd
zu definieren. Hier verlangt gcd
, dass seine Argumente und sein Rückgabetyp das Concept Integral
unterstützen. Mit anderen Worten: gcd
ist ein Funktions-Template, das Anforderungen an seine Argumente und seinen Rückgabewert stellt. Wenn ich den Syntactic Sugar entferne, kann man die wahre Natur von gcd
erkennen.
Das folgende Beispiel zeigt den semantisch äquivalente gcd
-Algorithmus, der eine requires-Clause verwendet.
template<typename T>
requires Integral<T>
T gcd(T a, T b) {
if( b == 0 ) return a;
else return gcd(b, a % b);
}
Die requires-Clause legt die Anforderungen an die Typ-Parameter von gcd
fest.
Mehr Details zu Concepts finden sich in den folgenden Beiträgen:
Nach dieser Einführung möchte ich Concepts definieren.
Definition von Concepts
Wenn das gesuchte Concept nicht zu den in C++20 vordefinierten gehört, lässt es sich definieren. Ich werde Concepte definieren, die sich durch die CamelCase-Syntax von den vordefinierten Concepts unterscheiden lassen. So heißt mein Concept für ein vorzeichenbehaftetes Integral SignedIntegral
, während das Standardkonzept in C++ den Namen signed_integral
besitzt.
Die Syntax zur Definition eines Concepts ist einfach:
template <template-parameter-list>
concept concept-name = constraint-expression;
Eine Concept-Definition beginnt mit dem Schlüsselwort template
und hat eine Template-Parameterliste. Die zweite Zeile ist etwas interessanter. Sie verwendet das Schlüsselwort concept
, gefolgt vom dem concept-name
und der constraint-expression
.
Eine constraint-expression
ist ein Compile-Zeit-Prädikat: eine Funktion, die zur Compilezeit ausgeführt wird und einen booleschen Wert zurückgibt. Dieses Compile-Zeit-Prädikat kann folgende Formen besitzen:
- Eine logische Kombination aus anderen Concepts oder Compile-Zeit-Prädikaten unter Verwendung von Konjunktionen (
&&
), Disjunktionen (||
) oder Negationen (!
) - Eine Requires Expression
- Einfache Anforderungen
- Typ-Anforderungen
- Zusammengesetzte Anforderungen
- Verschachtelte Anforderungen
Eine logische Kombination von anderen Concepts und Compile-Zeit_Prädikaten
Man kann Concept und Complie-Zeit-Prädikate mit Konjunktionen (&&
) und Disjunktionen (||
) kombinieren oder mit dem Ausrufezeichen (!
) negieren. Die Auswertung dieser logischen Kombination von Begriffen und Complie-Zeit-Prädikaten folgt der Kurzschlussauswertung. Kurzschlussauswertung bedeutet, dass die Auswertung eines logischen Ausdrucks automatisch stoppt, wenn sein Gesamtergebnis bereits feststeht.
Dank der vielen Complie-Zeit-Prädikate der Type Traits Library [8] stehen alle Werkzeuge zur Verfügung, die notwendig sind, um leistungsfähige Concepts zu erstellen.
Das folgende Beispiel zeigt die Concepts Integral
, SignedIntegral
und UnsignedIntegral
.
template <typename T> // (1)
concept Integral = std::is_integral<T>::value;
template <typename T> // (2)
concept SignedIntegral = Integral<T> && std::is_signed<T>::value;
template <typename T> // (3)
concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
Ich habe die type-traits-Funktion std::is_integral
verwendet, um das Concept Integral
zu definieren (1). Dank der Funktion std::is_signed
verfeinere ich das Concept Integral
zum Concept SignedIntegral
(2). Wenn ich schließlich das Concept SignedIntegral
negiere, erhalte ich das Concept UnsignedIntegral
(3). Zum Abschluss möchte ich das Concept noch anwenden.
// SignedUnsignedIntegrals.cpp
#include <iostream>
template <typename T>
concept Integral = std::is_integral<T>::value;
template <typename T>
concept SignedIntegral = Integral<T> && std::is_signed<T>::value;
template <typename T>
concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
void func(SignedIntegral auto integ) { // (1)
std::cout << "SignedIntegral: " << integ << '\n';
}
void func(UnsignedIntegral auto integ) { // (2)
std::cout << "UnsignedIntegral: " << integ << '\n';
}
int main() {
std::cout << '\n';
func(-5);
func(5u);
std::cout << '\n';
}
Ich verwende die Abbreviated Function Template Syntax, um die Funktion func
auf Grundlage der Concepts SignedIntegral
(1) und UnsignedIntegral
(2) zu überladen. Mehr über die Abbreviated Function Template Syntax steht in meinem vorherigen Artikel: C++20: Concepte - Syntactic Sugar [9]. Der Compiler wählt die erwartete Überladung aus:

Der Vollständigkeit halber sei erwähnt, dass das folgende Concept Arithmetic
die Disjunktion verwendet.
template <typename T>
concept Arithmetic =
std::is_integral<T>::value || std::is_floating_point<T>::value;
Wie geht's weiter?
In diesem Artikel habe ich die Concepts Integral
, SignedIntegral
und UnsignedIntegral
durch logische Kombinationen bestehender Concepts und Complie-Zeit-Prädikate definiert. In meinem nächsten Artikel wende ich Requires Expression an, um Concepts zu definieren.
(rme [10])
URL dieses Artikels:
https://www.heise.de/-6670266
Links in diesem Artikel:
[1] https://www.grimm-jaud.de/index.php/blog/category/c-20
[2] https://www.grimm-jaud.de/index.php/blog/tag/concepts
[3] https://en.cppreference.com/w/cpp/header/type_traits
[4] https://heise.de/-4599997
[5] https://heise.de/-4607890
[6] https://heise.de/-4615468
[7] https://heise.de/-4633761
[8] https://en.cppreference.com/w/cpp/header/type_traits
[9] https://heise.de/-4615468
[10] mailto:rme@ix.de
Copyright © 2022 Heise Medien