C++ Core Guidelines: Regeln für die Anwendung von Concepts
Mit sehr großer Wahrscheinlichkeit werden wir Concepts mit C++20 erhalten. Hier sind die Regeln der C++ Core Guidelines zu ihrer richtigen Anwendung.
- Rainer Grimm
Mit sehr großer Wahrscheinlichkeit werden wir Concepts mit C++20 erhalten. Hier sind die Regeln der C++ Core Guidelines zu ihrer richtigen Anwendung.
Zuerst gehe ich einen Schritt rückwärts. Was sind Concepts? Concepts sind Prädikate zur Compile-Zeit. Das heißt, dass sie zur Übersetzungszeit evaluiert werden und einen Wahrheitswert zurückgeben.
Die nächste Frage steht schon an. Was sind die Vorteile von Concepts?
Concepts
- erlauben Programmierern, direkt die Anforderungen an die Templates als Teil des Interfaces zu formulieren.
- unterstützen das Überladen von Funktionen und die Spezialisierung von Klassen-Templates, basierend auf den Anforderungen an die Templates.
- erzeugen deutlich verbesserte Fehlermeldungen, indem sie die Anforderungen an die Template-Parameter mit den aktuellen Template-Argumenten vergleichen.
- können als Platzhalter für die generische Programmierung verwendet werden.
- erlauben es, eigene Concepts zu definieren.
Nun geht es wieder einen Schritt vorwärts. Hier sind die vier Regeln für heute:
- T.10: Specify concepts for all template arguments
- T.11: Whenever possible use standard concepts
- T.12: Prefer concept names over auto for local variables
- T.13: Prefer the shorthand notation for simple, single-type argument concepts
Los geht es mit der ersten Regel.
T.10: Specify concepts for all template arguments
Zu dieser Regel gibt es nicht so viel hinzuzufügen. Wegen Korrektheit und Lesbakeit, solltest du Concepts für alle Template-Parameter verwenden. Concepts kannst du in der wortreichen Variante anwenden.
template<typename T>
requires Integral<T>()
T gcd(T a, T b){
if( b == 0 ){ return a; }
else{
return gcd(b, a % b);
}
}
Oder du kannst Concepts deutlich kompakter anwenden.
template<Integral T>
T gcd(T a, T b){
if( b == 0 ){ return a; }
else{
return gcd(b, a % b);
}
}
Im ersten Beispiel wende ich das Concept an, indem ich es in dem require-Abschnitt einsetze. Im zweiten Beispiel kommt das Concept einfach anstelle des Schlüsselworts typename oder class zum Einsatz. Das Concept Integral muss ein konstanter Ausdruck sein, der einen Wahrheitswert zurückgibt
Ich habe das Concept mit der Funktion std::is_integral aus der Type-Traits-Bibliothtek definiert.
template<typename T>
concept bool Integral(){
return std::is_integral<T>::value;
}
Ein Concept selbst zu definieren, sollte die Ausnahme sein.
T.11: Whenever possible use standard concepts
Okay, wenn möglich, solltest du Concepts aus der Guidelines Support Library (GSL) oder dem Ranges TS verwenden. Mal schauen, welche Concepts es gibt. Ich ignoriere die Conepts aus der GSL, denn sie sind meist schon im RangesTS definiert. Hier sind die Concepts des Ranges TS N4569: Working Draft, C++ Extension for Ranges.
Concepte der Kernsprache
- Same
- DerivedFrom
- ConvertibleTo
- Common
- Integral
- Signed Integral
- Unsigned Integral
- Assignable
- Swappable
Concepte für Vergleiche
- Boolean
- EqualityComparable
- StrictTotallyOrdered
Concept für Objekte
- Destructible
- Constructible
- DefaultConstructible
- MoveConstructible
- Copy Constructible
- Movable
- Copyable
- Semiregular
- Regular
Concepte für aufrufbare Einheiten
- Callable
- RegularCallable
- Predicate
- Relation
- StrictWeakOrder
Wenn du wissen willst, für was ein Concept steht, dann gibt dir das bereits erwähnte Dokument N4569 die Antwort. Die Definitionen der Concepts basieren auf der Type-Traits-Bibliothek. Hier sind zum Beispiel die Definitionen der Concepts Integral, Signed Integral und Unsigned Integral.
template <class T>
concept bool Integral() {
return is_integral<T>::value;
}
template <class T>
concept bool SignedIntegral() {
return Integral<T>() && is_signed<T>::value;
}
template <class T>
concept bool UnsignedIntegral() {
return Integral<T>() && !SignedIntegral<T>();
}
Die Funktionen std::is_integral<T> und std::is_signed<T> sind Prädikate aus der Type-Traits-Bibliothek.
Zusätzlich werden Namen in dem Text des C++-Standard verwendet, die die Anforderungen der Standard Template Library ausdrücken. Dies sind Concepts, die nicht geprüft werden, sondern Concepts, die die Anforderungen zum Beispiel von Algorithmen wie std::sort ausdrücken.
template< class RandomIt >
void sort( RandomIt first, RandomIt last );
Die erste Überladung von std::sort verlangt zwei RandomAccessIterator. Nun muss ich natürlich auflösen, was ein RandomAccessIterator ist.
- Ein RandomAccessIterator ist ein BidirectionalIterator, der auf jedes Element in konstanter Zeit verweisen kann.
- Ein BidirectionalIterator ist ein ForwardIterator, der sich in beide Richtungen verwenden lässt.
- Ein ForwardIterator ist ein Iterator, der Elemente lesen kann, auf die er verweist.
- Ein Iterator beschreibt einen Datentyp, der dazu verwendet werden kann, Elemente eines Containers zu identifizieren und zu traversieren.
Die Details zu den benannten Anforderungen, die im Text des C++ Standards verwendet werden, lassen sich unter cppreference.com schön nachlesen.
T.12: Prefer concept names over auto for local variables
auto ist ein uneingeschränktes Concept (Platzhalter), du solltest aber eingeschränkte Concepts verwenden. Du kannst eingeschränkte Concepts immer dann verwenden, wenn du uneingeschränkte Concepts (auto) eingesetzt hast. Wenn das keine einfache Regel ist?
Das folgende Beispiel verdeutlicht die Regel.
// constrainedUnconstrainedConcepts.cpp
#include <iostream>
#include <type_traits>
#include <vector>
template<typename T> // (1)
concept bool Integral(){
return std::is_integral<T>::value;
}
int getIntegral(int val){
return val * 5;
}
int main(){
std::cout << std::boolalpha << std::endl;
std::vector<int> myVec{1, 2, 3, 4, 5};
for (Integral& i: myVec) std::cout << i << " "; // (2)
std::cout << std::endl;
Integral b= true; // (3)
std::cout << b << std::endl;
Integral integ= getIntegral(10); // (4)
std::cout << integ << std::endl;
auto integ1= getIntegral(10); // (5)
std::cout << integ1 << std::endl;
std::cout << std::endl;
}
In der Zeile (1) habe ich das Concept Integral definiert. Daher iteriere ich in der Range-basierten for-Schleife (Zeile 2) über Ganzzahlen und die Variablen b und integ in Zeile (3) und (4) sind ebenfalls Ganzzahlen. In der Zeile (5) bin ich nicht so streng. In dieser wende ich ein uneingeschränktes Concept an.
Hier ist die Ausgabe des Programms.
T.13: Prefer the shorthand notation for simple, single-type argument concepts
Das Beispiel aus den C++ Core Guidelines schaut unschuldig aus, besitzt aber das Potenzial, die Art und Weise zu revolutionieren, wie wir Templates definieren. Hier ist es:
template<typename T> // Correct but verbose: "The parameter is
// requires Sortable<T> // of type T which is the name of a type
void sort(T&); // that is Sortable"
template<Sortable T> // Better (assuming support for concepts): "The parameter is of type T
void sort(T&); // which is Sortable"
void sort(Sortable&); // Best (assuming support for concepts): "The parameter is Sortable"
Dies Beispiel zeigt drei Variationen, das Funktions-Template sort zu definieren. Alle Variationen besitzen die gleiche Semantik und setzen das Concept Sortable voraus. Die letzte Variation wirkt wie eine Funktionsdeklaration, ist aber eine Funktions-Template Deklaration, da der Parameter ein Concept ist und nicht ein konkreter Datentyp. Das schreibe ich gerne nochmals: Dank des Concepts als Parameter wird sort zum Funktions-Template.
Wie geht's weiter?
Die C++ Core Guidelines schreibt: "Defining good concepts is non-trivial. Concepts are meant to represent fundamental concepts in an application domain." Gerne will ich mir in meinem nächsten Artikel genauer anschauen, was das bedeutet. ()