Aus Linux-Magazin 10/2021

C++-Core-Guidelines – Folge 60

© Aleksey Popov / 123RF.com

Die Zukunft von C++ liegt in Templates, zu denen die C++ Core Guidelines viele Regeln aufstellen. Einen wichtigen Aspekt von Templates stellen die Concepts dar.

Dieser Artikel beginnt mit einer kleinen Geschichte über zwei Irrwege. Sie soll belegen, warum Templates und insbesondere Concepts die Zukunft von C++ darstellen. In C++ gibt es zwei diametrale Optionen, mithilfe von Funktionen und Klassen Abstraktionen zu schaffen. Funktionen oder Klassen lassen sich für konkrete Datentypen oder generische Datentypen definieren. Im zweiten Fall nennen wir diese Funktions- oder Klassen-Templates. Warum führen beide Wege in die Irre?

Zu spezifisch

Es ist nahezu eine Herkulesaufgabe, für jeden konkreten Datentyp eine Funktion oder Klasse zu definieren. Um diese Last von unseren Schultern zu nehmen, kommen Typkonvertierungen ins Spiel. Doch was wie eine Rettung scheint, entpuppt sich wie in Listing 1 oft als Fluch. Im ersten Fall (Zeile 8), startet das Programm mit einem »double«- und endet mit einem »int«-Wert (Zeile 10). Im zweiten Fall (Zeile 12) startet es mit einem »bool«- und endet wieder mit einem »int«-Wert (Zeile 14). Abbildung 1 bringt das ganze Problem auf den Punkt.

Abbildung 1: Narrowing Conversion und Integral Promotion in Aktion.

Abbildung 1: Narrowing Conversion und Integral Promotion in Aktion.

Listing 1

Conversion und Promotion

#include <iostream>;
void needInt(int i){
 std::cout << "int: " << i << std::endl;
}
int main(){
 std::cout << std::boolalpha << std::endl;
 double d{1.234};
 std::c out << "double: " << d << std::endl;
 needInt(d);
 std::cout << std::endl;
 bool b{true};
 std::cout << "bool: " << b << std::endl;
 needInt(b);
 std::cout << std::endl;
}

Der Aufruf von »getInt(int a)« mit einem »double«-Wert verursacht eine Narrowing Conversion, also eine Konvertierung mit Verlust der Datengenauigkeit. Das war sicher nicht die Absicht des Autors.

Anders herum ist es aber auch nicht besser. Ruft man »getInt(int a)« mit einem »bool«-Wert auf, findet eine Integral Promotion statt, und der »bool«-Wert wird auf einen »int«-Wert aufgeblasen. Überrascht?

Viele C/C++-Entwickler wissen nicht, was passiert, wenn man zwei Wahrheitswerte addiert, etwa mit »auto res = true + false«. Die entscheidende Frage lautet: Welchen Wert hat die Variable »res« und welchen Datentyp hat sie? C++ Insights [1] stellt die Magie der Integral Promotion dar (Abbildung 2). Die beiden Wahrheitswerte »true« und »false« werden nach »int« konvertiert, sodass das Ergebnis der Addition 1 lautet.

Abbildung 2: Integral Promotion in C++ Insights.

Abbildung 2: Integral Promotion in C++ Insights.

Die ganze Magie der Typkonvertierungen in C/C++ scheint nur aus praktischen Gründen zu existieren: Sie erlaubt mit der Unzulänglichkeit umzugehen, dass Funktionen nur spezifische Datentypen annehmen können. Wenden wir nun die zweite Option an und nutzen keine spezifischen, sondern generische Datentypen – vielleicht stellen in diesem Fall ja gerade Templates die Rettung dar.

Zu generisch

Sortieren ist eine generische Idee. Man sollte sie auf jeden Container anwenden können, dessen Elemente sich sortieren lassen. Daher kommt in Listing 2 der Algorithmus »std::sort« [2] der Standard Template Library auf einer »std::list« [3] zum Einsatz. Abbildung 3 zeigt die resultierende Fehlermeldung des GCC-Compilers, die sich kaum dechiffrieren lässt. Was läuft hier falsch?

Listing 2

Sortierung einer std::list

#include <algorithm>
#include <list>
int main() {
 std::list<int> myList{1, 10, 3, 2, 5};
 std::sort(myList.begin(), myList.end());
}
Abbildung 3: Die Fehlermeldung des GCC-Compilers beim Versuch, eine &raquo;std::list&laquo; zu sortieren.

Abbildung 3: Die Fehlermeldung des GCC-Compilers beim Versuch, eine »std::list« zu sortieren.

Vielleicht hilft ein genauerer Blick auf die verwendete Überladung von »std::sort« in Listing 3. »std::sort« verwendet den seltsam klingenden Namen »RandomIt«. Er steht für einen Random Access Iterator, der den wahlfreien Zugriff erlaubt. Das ist der Grund für die kaum zu entziffernde Fehlermeldung, für die Templates berühmt-berüchtigt sind.

Listing 3

Überladen von std::sort

template<class RandomIt>
void sort(RandomIt first, RandomIt last);

Eine »std::list« bietet nur einen Bidirectional Iterator an. Ein solcher ermöglicht es aber lediglich, die »std::list« vorwärts und rückwärts zu traversieren. Damit erfüllt er – und somit die »std::list« – nicht die notwendigen Anforderungen an »std::sort«. Die Struktur einer »std::list« in Abbildung 4 macht diese Einschränkung offensichtlich.

Abbildung 4: Die Struktur einer &raquo;std::list&laquo;.

Abbildung 4: Die Struktur einer »std::list«.

Dank der Concepts in C++20 lassen sich diese Anforderungen wie ein Random Access Iterator direkt im Code ausdrücken. Damit endet diese Geschichte über die zwei Irrwege, und die ersten drei Regeln zu den C++ Core Guidelines stehen im Fokus:

  • T.1: Use templates to raise the level of abstraction of code [4].
  • T.2: Use templates to express algorithms that apply to many argument types [5].
  • T.3: Use templates to express containers and ranges [6].

Vereinfachend gesagt, lassen sich die drei Regeln auf ein Prinzip reduzieren: Verwende Templates, um das Abstraktionslevel von Funktionen und Klassen zu steigern.

Der Tradition der C++ Core Guidelines folgend, sind zu Anfang die verwendeten Concepts in folgenden Codebeispielen auskommentiert. Dies ist zwei Tatsachen geschuldet: Zum einen stellen die verwendeten Concepts die falsche Abstraktion dar, zum anderen setzen sie einen Compiler voraus, der den C++20-Standard unterstützt.

Templates dienen laut der Regel T.1 dazu, das Level der Abstraktion zu steigern. Warum stellen die beiden verwendeten Concepts – und damit die Funktions-Templates – in Listing 4 keine richtige Abstraktion dar? Beide Concepts fallen viel zu spezifisch aus und basieren auf konkreten Operationen wie Inkrementieren und Addieren. Concepts sollen nicht syntaktische Einschränkungen modellieren, sondern semantischen Kategorie wie »Arithmetic«.

Listing 4

Die Concepts Incrementable und Addable

template<typename T>
// requires Incrementable<T>
T sum1(vector<T>& v, T s) {
  for (auto x : v) s += x;
  return s;
}
template<typename T>
// requires Addable<T>
T sum2(vector<T>& v, T s) {
  for (auto x : v) s = s + x;
  return s;
}

Listing 5 setzt die Anforderung mustergültig um. Der generische Algorithmus »sum« stellt zwei Anforderungen: Einerseits müssen sich die Elemente des Containers in der Range-basierten For-Schleife traversieren lassen. Andererseits muss die Summationsvariable »s« die Addition unterstützen. Zugegeben, der generische Algorithmus »sum« besitzt noch eine große Unzulänglichkeit: Die Concepts sind auskommentiert. Das lässt sich dank C++20 einfach beheben. Die aktuellen Versionen von GCC und Clang sowie der Microsoft Compiler (mit kleinen Einschränkungen) sprechen bereits fließend Concepts.

Listing 5

Das Funktions-Template sum

template<typename Cont, typename T>
// requires Container<Cont>
// && Arithmetic<T>
T sum(const Cont& v, T s) {
  for (auto x : v) s += x;
  return s;
}

Concepts verwenden

Das Programm in Listing 6 setzt den generischen »sum«-Algorithmus in drei Variationen um. @L:Die generischen Funktionen »sum1« (Zeile 9 bis 15), »sum2« (Zeile 17 bis 21) und »sum3« (Zeile 23 bis 26) sind äquivalent. Jede der drei fordert, dass der Container »Cont« das Concept »std::ranges::input_range« und die Summationsvariable »s« das Concept »Arithmetic« umsetzt.

Listing 6

Drei Variationen

#include <iostream>
#include <ranges>
#include <set>
#include <type_traits>
#include <vector>
template<typename T>
concept Arithmetic = std::is_arithmetic<T>::value;
template<typename Cont, typename T>
  requires std::ranges::input_range<Cont>
  && Arithmetic<T>
T sum1(const Cont& v, T s) {
  for (auto x : v) s += x;
  return s;
}
template<std::ranges::input_range Cont, Arithmetic T>
T sum2(const Cont& v, T s) {
  for (auto x : v) s += x;
  return s;
}
Arithmetic auto sum3(const std::ranges::input_range auto& v, Arithmetic auto s) {
  for (auto x : v) s += x;
  return s;
}
int main() {
  std::cout << '\n';
  std::vector<int> myVec{1, 2, 3, 4, 5};
  std::set<int> mySet{10, 20, 30, 40, 50};
  int res{};
  std::cout << "sum1(myVec, res): " << sum1(myVec, res) << '\n';
  std::cout << "sum1(mySet, res): " << sum1(mySet, res) << '\n';
  std::cout << '\n';
  std::cout << "sum2(myVec, res): " << sum2(myVec, res) << '\n';
  std::cout << "sum2(mySet, res): " << sum2(mySet, res) << '\n';
  std::cout << '\n';
  std::cout << "sum3(myVec, res): " << sum3(myVec, res) << '\n';
  std::cout << "sum3(mySet, res): " << sum3(mySet, res) << '\n';
  std::cout << '\n';
}

Das Concept »std::ranges::input_range« aus der Ranges-Bibliothek fordert im Wesentlichen, dass sich der Container vorwärts traversieren lässt. Das Concept »Arithmetic« lässt sich hingegen mit der Funktion »std::is_arithmetic« der Type-Traits-Bibliothek [7] definieren. Sie prüft, ob es sich bei ihrem Argument um eine Ganzzahl oder eine Gleitkommazahl handelt. Ein Concept muss ein Compile-Zeit-Prädikat sein, also ein Ausdruck, der zur Compile-Zeit zu »true« oder »false« evaluiert.

Da die Container der Standard Template Library das Concept »std::ranges::input_range« umsetzen, lassen sich alle Container in den generischen »sum«-Funktionen verwenden, und damit auch »std::vector« (Zeile 30) und »std::set« (Zeile 31). Die Funktionen »sum1«, »sum2« und »sum3« wenden die zwei Concepts unterschiedlich an.

So fordert »sum1« in den sogenannten Require Clauses in den Zeilen 10 und 11, dass die beiden Concepts erfüllt sein müssen. Hingegen verwendet die Funktion »sum2« eingeschränkte Type-Parameter. Anstelle von »template <typename Cont, typename T>« kommt »template <std::ranges::input_range Cont, Arithmetic T>« zum Einsatz.

Am interessantesten ist sicher der Einsatz von Concepts in der generischen Funktion »sum3«. Verwendet man statt der konkreten Datentypen wie in diesem Fall sogenannte eingeschränkte Platzhalter (Zeilen 30 und 31), erzeugt der Compiler implizit ein Funktions-Template. Für diese neue Syntax zur Definition von Funktions-Templates hat sich der sperrige Name Abbreviated Function Templates Syntax etabliert. Abbildung 5 zeigt die Ausgabe des Programms.

Abbildung 5: Struktur einer &raquo;std::list&laquo;.

Abbildung 5: Struktur einer »std::list«.

Tatsächlich kennt die Ranges-Bibliothek von C++20 mehrere Concepts, um die Container der Standard Template Library zu klassifizieren. Die Tabelle “Range Concepts” stellt die vier wichtigsten Klassen kompakt dar. Wie muss man diese Tabelle lesen?

Concept

Iterator Property

Containers

»std::ranges::input_range«

»++It«, »It++«, »*it«, »It == It2«, »It != It2«

»std::unordered_set« »std::unordered_map« »std::unordered_multiset« »std::unordered_multimap« »std::forward_list«

»std::ranges::bidirectional_range«

»–It«, »It–«

»std::set« »std::map« »std::multiset« »std::multimap« »std::forward_list«

»std::ranges::random_acccess_range«

»It[i]« »It+=n«, »It -=n« »It + n«, »It – n« »n + It« »It – It2« »It < It2«, »It <= It2« »It > It2« »It >= It2«

»std:deque«

»std:ranges::contiguous_range«

»std::array« »std::vector« »std::string«

Ein Container, der das Concept »std::ranges::contiguous_range« unterstützt, unterstützt auch alle vorherigen Concepts in der Tabelle, wie »std::ranges::random_access_range«, »std::ranges::bidirectional_range« und »std::ranges::input_range«. Das gilt für die anderen Container der Tabelle ebenso.

Iterator Property steht für die Mächtigkeit, die ein Iterator dem entsprechenden Container anbietet. So unterstützt der Iterator, den »std::list« als »std::ranges::bidirection_range« bereitstellt, die Rückwärtsiteration in der Präinkrement- (»–It«) und Postinkrement-Variante (»It–«). Darüber hinaus unterstützt er alle weiteren Operationen, die die Iteratoren eines »std::ranges::input_range« bereitstellen.

Damit schließt sich der Kreis. Mithilfe der Concepts für Iteratoren oder Container lässt sich »std::sort« aus Listing 2 so definieren, dass ein ungültiger Aufruf eine wohldefinierte Fehlermeldung erzeugt. Listing 7 stellt zwei schematische Implementierungen von »std::sort« vor.

Listing 7

std::sort mit Concepts

#include <concepts>
#include <ranges>
#include <list>
#include <vector>
template<std::random_access_iterator RandomIt>
void sort1(RandomIt first, RandomIt last){ }
template<std::ranges::random_access_range RandomAccessRange>
void sort2(RandomAccessRange Cont) { };
template<class RandomIt>
void sort( RandomIt first, RandomIt last );
int main() {
  std::vector<int> myVec{1, 10, 3, 2, 5};
  std::list<int> myList{1, 10, 3, 2, 5};
  sort1(myVec.begin(), myVec.end());
  sort1(myList.begin(), myList.end());
  sort2(myVec);
  sort2(myList);
}

Während »std::sort1« (Zeile 6 und 7) der Signatur von »std::sort« in der Standard Template Library genügt, folgt »sort2« (Zeile 9 und 10) der Implementierung von »std::ranges::sort«. Letzteres ist Bestandteil der Ranges-Bibliothek und agiert auf dem ganzen Container. Dementsprechend benötigt »sort1« zwei Iteratoren und das Concept »std::random_access_iterator«, »sort2« dagegen eine Range und das Concept »std::random_access_range«.

Bei den Ranges handelt es sich um eine Erweiterung der Container der STL. Das Concept »std::random_access_iterator« erfordert die Header-Datei »concepts«, das Concept »std::random_access_range« die Header-Datei »ranges«. Weder »sort1« noch »sort2« lassen sich mit der »std::list« (Zeile 20) aufrufen, noch mit den Iteratoren, die »std::list« zur Verfügung stellt (Zeile 17). Dies gilt jedoch nicht für »std::vector« (Zeile 19) oder den durch »std::vector« bereitgestellten Iterator (Zeile 16).

Abbildung 6 und Abbildung 7 stellen die wesentlichen Zeilen des Clang-Compilers beim vergeblichen Versuch dar, das Programm zu übersetzen.

Abbildung 6: Fehlgeschlagene Instanziierung von &raquo;sort1(myList.begin(), myList.end())&laquo;.

Abbildung 6: Fehlgeschlagene Instanziierung von »sort1(myList.begin(), myList.end())«.

Abbildung 7: Fehlgeschlagene Instanziierung von &raquo;sort2(myList)&laquo;.

Abbildung 7: Fehlgeschlagene Instanziierung von »sort2(myList)«.

Wie geht es weiter?

Wie Sie sicher schon vermuten, haben die C++ Core Guidelines noch viel mehr Regeln rund um Templates zu bieten. So gehen die Guidelines – und der nächste Artikel dieser Reihe – auf die richtige Parametrisierung der Algorithmen der Standard Template Library ein und stellen Templates als Lösung vor, um Funktionen und Klassen für die konkreten Datentypen zu spezialisieren. (jcb/jlu)

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben