C++ Core Guidelines: Regeln für die Anwendung von Concepts

Modernes C++  –  1 Kommentare

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:

Los geht es mit der ersten Regel.

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.

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.

  • Same
  • DerivedFrom
  • ConvertibleTo
  • Common
  • Integral
  • Signed Integral
  • Unsigned Integral
  • Assignable
  • Swappable
  • Boolean
  • EqualityComparable
  • StrictTotallyOrdered
  • Destructible
  • Constructible
  • DefaultConstructible
  • MoveConstructible
  • Copy Constructible
  • Movable
  • Copyable
  • Semiregular
  • Regular
  • 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.

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.

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.

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.