C++20: Zwei Extreme und die Rettung dank Concepts
Nach dem groben Überblick zu C++20 ist es nun an der Zeit, die Features genauer unter die Lupe zu nehmen. Hierfür gibt es keinen besseren Einstieg als Concepts.
Im letzten Blog-Artikel habe ich meinen Überblick zu C++20 abgeschlossen [1]. Jetzt ist es an der Zeit, die Features genauer unter die Lupe zu nehmen. Hierfür gibt es keinen besseren Einstieg als Concepts.
Zugegebenermaßen, bin ich ein großer Fan von Concepts und daher parteiisch. Zuerst möchte ich Concepts motivieren.
Zwei Extreme
Bis C++20 hatten wir in C++ zwei diametrale Optionen, mithilfe von Funktionen und Klassen Abstraktionen zu schaffen. Funktionen oder Klassen ließen sich für konkrete Datentypen oder generische Datentypen definieren. Im zweiten Fall nennen wir diese Funktions- oder Klassen-Templates. Warum sind beide Wege falsch?
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 oft als Fluch:
// tooSpecific.cpp
#include <iostream>
void needInt(int i){
std::cout << "int: " << i << std::endl;
}
int main(){
std::cout << std::boolalpha << std::endl;
double d{1.234}; // (1)
std::cout << "double: " << d << std::endl;
needInt(d); // (2)
std::cout << std::endl;
bool b{true}; // (3)
std::cout << "bool: " << b << std::endl;
needInt(b); // (4)
std::cout << std::endl;
}
Im ersten Fall (Zeile 1), starte ich mit einem double
- und ende mit einem int
-Wert (Zeile 2). Im zweiten Fall (Zeile 3) starte ich mit einem bool
- und ende wieder mit einem int
-Wert (Zeile 4).
- Narrowing Conversion
Der Aufruf von getInt(int a)
mit einem double
-Wert verursacht "narrowing conversion". Dieses ist eine Konvertierung mit Verlust der Datengenauigkeit. Das war sicherlich nicht die Absicht des Autors.
- Integral Promotion
Anders herum ist es aber auch nicht besser. Wird getInt(int a)
mit einem bool
-Wert aufgerufen, wird dieser auf einen int
-Wert aufgeblasen. Überrascht? Viele C++ Entwickler wissen nicht, was passiert, wenn zwei Wahrheitswerte addiert werden:
template <typename T>
auto add(T first, T second){
return first + second;
}
int main(){
add(true, false);
}
C++ Insights [2] zeigt die ganze Wahrheit:
Die Template-Instanziierung des Funktions-Templates add
erzeugt eine vollständige Spezialisierung (Zeilen 6 bis 12), die den Rückgabetyp int
besitzt.
Meine feste Überzeugung ist es, dass wir nur aus praktischen Gründen die ganze Magie der Typkonvertierungen in C/C++ besitzen, um mit der Unzulänglichkeit umgehen zu können, dass Funktionen nur spezifische Datentypen annehmen können.
Wenden wir nun die zweite Option an und nutzen keine spezifischen, sondern generische Datentypen. Vielleicht sind ja gerade Templates unsere Rettung.
Zu generisch
Hier ist mein erster Versuch: Sortieren ist ein ganz allgemeine Idee. Daher sollte sie auf jeden Container anwendbar sein, falls sich die Elemente des Containers sortieren lassen. Lasse mich std::sort
auf eine std::list
anwenden:
// sortList.cpp
#include <algorithm>
#include <list>
int main(){
std::list<int> myList{1, 10, 3, 2, 5};
std::sort(myList.begin(), myList.end());
}
Wow! Dies passiert, wenn du versuchst, das kleine Programm zu übersetzen:
Diese Fehlermeldung will ich nicht dechiffrieren. Was läuft hier falsch? Vielleicht hilft ein genauerer Blick auf die verwendete Überladung von std::sort
.
template< class RandomIt >
void sort( RandomIt first, RandomIt last );
std::sort
verwendet den seltsam klingenden Name RandomIt. Es steht für einen random access iterator. Dies ist der Grund für die überwältigende Fehlermeldung, für die Templates berühmt-berüchtigt sind. Eine std::list
bietet nur einen bidirectional iterator an, aber std::sort
benötigt einen random access iterator. Die Struktur einer std::list
macht diese Einschränkung offensichtlich.
Wenn du genauer die Dokumentation zu std::sort
auf cppreference.com [3] studierst, findest du etwas sehr Interessantes: Typanforderungen an std::sort
.
Concepte als Rettung
Concepts sind die Rettung, denn sie definieren semantische Einschränkungen auf Template-Parametern.
Hier sind die bereits erwähnten Typanforderungen zu std::sort
.
- RandomIt must meet the requirements of ValueSwappable [4] and LegacyRandomAccessIterator. [5]
- The type of dereferenced RandomIt must meet the requirements of MoveAssignable [6] and MoveConstructible [7].
- Compare must meet the requirements of Compare [8].
Die Typanforderungen an std::sort
sind Concepte. Mein Artikel "C++20: Die vier großen Neuerungen [9]" gibt eine kompakte Einführung zu Concepts. Insbesondere fordert std::sort
einen LegacyRandomAccessIterator
. [10] Darauf will ich gerne einen genaueren Blick werfen. Das Beispiel von cppreference.com [11] habe ich ein wenig poliert:
template<typename It>
concept LegacyRandomAccessIterator =
LegacyBidirectionalIterator<It> && // (1)
std::totally_ordered<It> &&
requires(It i, typename std::incrementable_traits<It>::difference_type n) {
{ i += n } -> std::same_as<It&>; // (2)
{ i -= n } -> std::same_as<It&>;
{ i + n } -> std::same_as<It>;
{ n + i } -> std::same_as<It>;
{ i - n } -> std::same_as<It>;
{ i - i } -> std::same_as<decltype(n)>;
{ i[n] } -> std::convertible_to<std::iter_reference_t<It>>;
};
Dies ist die entscheidende Beobachtung. Ein Datentyp It
unterstützt das Concept LegacyRandomAccessIterator
genau dann, wenn er das Concept LegacyBidirectionalIterator
(Zeile 1) und die weiteren Anforderungen unterstützt. Zum Beispiel sagt die Anforderung in Zeile 2 für einen Wert des Datentyps It
aus: { i += n }
muss gültig sein und eine Referenz auf It
zurückgeben. Um mit meiner Story zum Abschluss zu kommen. std::list
unterstützt nur einen LegacyBidirectionalIterator
.
Zugegeben, dieser Abschnitt war sehr technisch. Jetzt folgt die Praxis. Mit Concepts wirst du eine einfache Fehlermeldung wie die folgende erhalten, wenn du std::sort
mit einer std::list
erhältst:
Sorry, die Fehlermeldung war eine Ente, denn kein Compiler setzt zum jetzigen Zeitpunkt die C++20-Syntax für Concepts vollständig um. MSVC 19.23 unterstützt Concepts teilweise, GCC eine vorherige Version von Concepts. cppreference.com [12] liefert mehr Details zur aktuellen Compilerunterstützung von Concepts.
Habe ich gesagt, dass GCC ein vorherige Version von Conceps unterstützt?
Die lange, lange Geschichte
Das erste Mal, als ich um 2005/2006 mit Concepts in Berührung kam, erinnerten sie mich an Haskells Typklassen. Typklassen in Haskell sind Interface für ähnliche Datentypen. Das Bild zeigt einen Ausschnitt aus Haskells Typklassenhierachie:
Doch C++ Concepts unterscheiden sich von Haskells Typklassen. Hier sind ein paar Beobachtungen.
- In Haskell muss ein Datentyp eine Instanz einer Typklasse sein. In C++20 muss ein Datentyp die Anforderungen eines Concepts erfüllen.
- Concepts lassen sich auch auf "non-type parameter" anwenden. Zum Beispiel kann die Zahl 5 als non-type parameter verwendet werden. Wenn du ein
std::array
vonint
s mit 5 Elementen haben möchtest, wendest du diesen non-type parameter an:std::array<int, 5> myVec
. - Concepts besitzen keine Laufzeitkosten.
Ursprünlich sollten Concepts bereits das große Feature von C++11 sein. In dem Standardisierungsmeeting in Frankfurt wurden sie aber im Juli 2009 entfernt. Das Zitat von Bjarne Stroustrup spricht für sich selbst: "The C++Ox concept design evolved into a monster of complexity." Ein paar Jahre später war der nächste Versuch, Concepts zu standardisieren, wieder erfolglos. Concept lite wurden aus C++17 entfernt. Nun bekommen wir sie aber mit C++20.
Wie geht's weiter?
Mit meinem nächsten Artikel knüpfe ich natürlich an Concepts an. Ich werde viele Beispiele zu semantischen Einschränkungen auf Template-Parameter vorstellen. ( [13])
URL dieses Artikels:
https://www.heise.de/-4594834
Links in diesem Artikel:
[1] https://www.heise.de/blog/C-20-Ueberblick-zur-Concurrency-4585408.html
[2] https://cppinsights.io/s/22249b6a
[3] https://en.cppreference.com/w/cpp/algorithm/sort
[4] https://en.cppreference.com/w/cpp/named_req/ValueSwappable
[5] https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator
[6] https://en.cppreference.com/w/cpp/named_req/MoveAssignable
[7] https://en.cppreference.com/w/cpp/named_req/MoveConstructible
[8] https://en.cppreference.com/w/cpp/named_req/Compare
[9] https://www.heise.de/blog/C-20-Die-grossen-Vier-4568956.html
[10] https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator
[11] https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator
[12] https://en.cppreference.com/w/cpp/compiler_support
[13] mailto:rainer@grimm-jaud.de
Copyright © 2019 Heise Medien