Definition von Concepts

Es gibt zwei Möglichkeiten, ein Concept zu definieren: Bestehende Concepts und Compile-Zeit-Prädikate lassen sich kombinieren oder Requires Expression anwenden.

In Pocket speichern vorlesen Druckansicht 14 Kommentare lesen

(Bild: Shutterstock.com/Kenishirotie)

Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Bevor ich über C++20 und Concepte schreibe, möchte ich eine kurze Anmerkung machen.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Ich habe bereits mehr als 80 Artikel über C++20 und 20 über Concepts 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:

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 von auto 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, 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.

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

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 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. 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;

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)