Die Guideline Support Library (GSL) erweist sich als ideale Zutat der C++ Core Guidelines, hilft sie doch den C++-Entwicklern die Regeln der Core Guidelines umzusetzen. Ihr Ziel besteht vor allem darin, im Vergleich zu konventionellem Code keinen Overhead zu verursachen.
Die Guideline Support Library [1] besteht eigentlich nur aus Headern. Ihre Funktionen befinden sich im Namensraum »gsl« und lassen sich durch eine einfache »include«-Anweisungen in Quellcodedateien einbinden. Die bekannteste Implementierung stammt von Microsoft und wartet direkt auf Github [2]. Sie setzt den C++14-Standard voraus und läuft auf mehreren Plattformen. Die bekanntesten sind:
- Windows mit Visual Studio 2015
- Windows mit Visual Studio 2017
- Linux mit Clang/LLVM 3.6
- Linux mit GCC 5.1
Es gibt weitere Implementierungen auf Github. Besonders interessant ist GSL-lite [3] von Martin Moene, das sich bereits mit den Standards C++98, C++03 sowie C++11 einsetzen lässt.
Wermutstropfen
Die Guideline Support Library besitzt zum jetzigen Zeitpunkt noch zwei große Mankos: Es gibt keine gute Dokumentation und es fehlen Tutorials. Wer sich die Bibliothek im Detail anschauen möchte, muss sie also installieren, um ihre Unittests zu analysieren. Verglichen damit erweist sich das Installieren und Verwenden von Microsofts GSL-Implementierung sowohl unter Windows als auch unter Linux als sehr einfach.
Komponentenkleber
C++-Entwicklern stellt sich zunächst die Frage: Welche Komponenten enthält die GSL? Tabelle 1 gibt einen Überblick zu den fünf Kandidaten.
|
Komponente |
Features |
|||||
|---|---|---|---|---|---|---|
|
Views |
»span<T>« |
»string_span<T>« |
»czstring« |
»wzstring« |
||
|
Owner |
»owner<T>« |
»unique_ptr<T>« |
»shared_ptr<T>« |
»dyn_array<T>« |
»stack_array<T>« |
|
|
Assertions |
»Expects()« |
»Ensures()« |
||||
|
Utilities |
»narrow« |
»narrow_cast()« |
»not_null<T>« |
»finally« |
||
|
Concepts |
»Range« |
»String« |
»Number« |
»Sortable« |
»Pointer« |
»[…]« |
Ein forschender Blick auf die Datentypen ruft aber womöglich Verwunderung hervor. Denn die GSL-lite unterstützt eigene Smart Pointer »gsl::unique_ptr« und »gsl::shared_ptr«, die doch bereits zum C++11-Standard gehören. Dies entpuppt sich als ein deutlicher Mehrwert, weil die GSL-lite mit dem C++98-Standard läuft. Smart Pointer funktionieren also über den Umweg der GSL bereits mit einem C++98-Compiler.
Viele der Funktionen und Datentypen der GSL werden zudem wohl in C++20 landen. Das gilt insbesondere für Concepts und Assertions, aber auch für die verbleibenden Komponenten der GSL.
Views
Eine View ist niemals ein Besitzer (Owner). Im Falle von »gsl::span<T>« repräsentiert sie einen Bereich zusammenhängenden Speichers, ohne diesen aber zu besitzen. Dabei kann es sich um ein Array, einen Zeiger mit seiner Länge oder einen »std::vector« handeln.
Diese Aussage gilt für »gsl::string_span<T>« und den Null-terminierten C-String »gsl::czstring« ebenso wie für «»gsl::wzstring«. Der Unterschied zwischen »czstring« und »wzstring« besteht darin, dass der erste auf einem Zeichen »char« und der letzte auf einem breiten Zeichen »wchar_t« basiert.
Worin besteht nun der große Mehrwert von Views wie »gsl::span<T>«? Übergibt ein Entwickler ein C-Array an eine Funktion, entstehen Sicherheitsprobleme. Der Vorgang reduziert nämlich das C-Array auf einen Zeiger auf ihr erstes Element. Formal heißt das Decay [4] und hat konkret zur Folge, dass jegliche Längeninformation zum C-Array verloren geht. Das öffnet Off-by-one-Fehlern [5] Tür und Tor.
Kommt hingegen »gsl::span<T>« als Parameter einer Funktion zum Einsatz, entsteht ein Mehrwert, weil es automatisch die Länge seines Arguments bestimmt. Empfängt die Funktion ihre Argumente hingegen über einen Zeiger, ist der Entwickler gezwungen die Länge explizit zu übergeben. Listing 1 stellt beide Techniken einander gegenüber.
Listing 1
Kopieren in C-Arrays und Vektoren
01 template <typename T>
02 void copy_n(const T* p, T* q, int n){
03 [...]
04 }
05
06 template <typename T>
07 void copy(gsl::span<const T> src, gsl::span<T> des){
08 [...]
09 }
10
11 int main(){
12
13 int arr1[] = {1, 2, 3};
14 int arr2[] = {3, 4, 5};
15
16 std::vector<int> vec1{1, 2, 3, 4};
17 std::vector<int> vec2{5, 6, 7, 8};
18
19 copy_n(arr1, arr2, 3);
20
21 copy(arr1, arr2);
22 copy(vec1, vec2);
23
24 }
Im Gegensatz zur Funktion »copy_n()« (Zeile 2) braucht der Entwickler bei der Funktion »copy()« (Zeile 7) nicht die Anzahl seiner Elemente anzugeben. Dies trifft sowohl auf das C-Array in Zeile 21 als auch auf den Vektor in Zeile 22 zu. Damit verschwindet dank »gsl::span<T>« ein typischer Fehler von C- oder C++-Programmen.
Owner
Auch erwähnenswert ist, dass es in der GSL verschiedene Besitzverhältnisse gibt. Über die Smart Pointer »std::unique_ptr« und »std::shared_ptr« war bereits 2013 in den Artikeln unter [6] und [7] die Rede. Die dort versammelten Erkenntnisse betreffen natürlich auch »gsl::unique_ptr« und »gsl::shared_ptr«.
Der Zeiger »gsl::owner<T>« verweist, wie unschwer zu erraten ist, auf den Besitzer seiner referenzierten Ressource. Er sollte immer dann zum Einsatz kommen, wenn der Entwickler keinen Smart Pointer oder Container einsetzen kann. Der entscheidende Unterschied zwischen »gsl::owner<T>« und einem Smart Pointer wie »std::unique_ptr« und »std::shared_ptr« besteht darin, dass »gsl::owner<T>« seine Ressource explizit freigeben muss. Smart Pointer entlassen ihre Ressourcen dagegen automatisch in die Freiheit.
Ist ein Zeiger hingegen nicht als »gsl::owner<T>« deklariert, darf der Entwickler ihn auch nicht als Besitzer betrachten. Das zieht insbesondere nach sich, dass der Zeiger seine Ressource nicht freigeben darf.
Doch das ist noch nicht alles. Die GSL bietet mit »gsl::dyn_array<T>« und »gsl::stack_array<T>« auch zwei neue Array-Typen an:
- »gsl::dyn_array<T>« ist ein Array fester Länge, das die Bibliothek auf dem Heap anlegt. Die Länge spezifiziert sie dabei zur Laufzeit.
- »gsl::stack_array<T>« ist ein Array fester Länge, das die Bibliothek auf dem Stack anlegt. Auch hier legt sie die Länge zur Laufzeit fest.
Im nächsten Schritt soll es nun mit den Assertions weitergehen.
Assertions
Dank »Expects()« und »Ensures()« lassen sich auch Vor- und Nachbedingungen an eine Funktion stellen, wie es Listing 2 demonstriert.
Listing 2
Vor- und Nachbedingungen an Funktion stellen
01 int area(int height, int width)
02 {
03 Expects(height > 0);
04 auto res = height * width;
05 Ensures(res > 0);
06 return res;
07 }
Mit der GSL muss der Entwickler diese Vor- und Nachbedingungen aktuell direkt im Funktionskörper platzieren. Ab C++20 darf er sie dann in der Funktionsdeklaration angeben. Beide Funktionen sind Bestandteil der Kontrakte in C++20, die bereits Gegenstand des vorigen Artikels [8] waren.
Utilities
Mit den Konvertierungen »gsl::narrow_cast<T>« und »gsl::narrow<T>« bietet die Guideline Support Library zudem zwei neue Varianten an:
- »gsl::narrow_cast<T>« ist eine »static_cast<T>«-Typkonvertierung, die ihre Intention explizit ausdrückt. Eine verengende Konvertierung (Narrowing Conversion) ist möglich. Verengende Konvertierung bezeichnet eine Konvertierung mit Verlust der Datengenauigkeit. Das ist zum Beispiel dann der Fall, wenn der Entwickler einem »int«-Wert einen »double«-Wert zuweist.
- »gsl::narrow<T>« ist ebenfalls eine »static_cast<T>«-Typkonvertierung. Sie wirft allerdings eine »narrowing_error«-Ausnahme, falls eine »gsl:: narrow<T>(t)«-Konvertierung den Wert von »t« verändert.
Die GSL enthält noch weitere praktische Werkzeuge. So steht »gsl::not_null<T*>« für einen Zeiger, der kein Nullzeiger (»nullptr«) sein darf. Kommt er zum Einsatz und setzt der Entwickler einen »gsl::not_null<T*>«-Zeiger auf einen »nullptr«, quittiert dies der Compiler mit einer Fehlermeldung. Selbst Smart Pointer wie »std::unique_ptr« oder »std::shared_ptr« sind in einem »gsl::not_null<T*>«-Zeiger einsetzbar.
Der typische Einsatzbereich für »gsl::not_null<T*>« sind Funktionsparameter und der Rückgabetyp einer Funktion. Damit muss der Entwickler nicht vor jedem Einsatz eines Zeigers (Listing 3) prüfen, ob dieser ein »nullptr« ist.
Listing 3
Prüfung auf nullptr
01 // p cannot be a nullptr 02 int getLength1(gsl::not_null<const char*> p); 03 04 // p can be a nullptr 05 int getLength2(const char* p);
Die Signaturen der Funktionen bringen ihre Absicht direkt auf den Punkt. Die Funktion »getLength2()« lässt sich auch mit einem »nullptr« aufrufen.
Ist ein angemessenerer Umgang mit Ressourcen über Smart Pointer oder Container nicht möglich, kommt »finally« als Rettungsoption ins Spiel. Sie erlaubt es, eine Funktion zu registrieren, die C++ automatisch ausführt, wenn sie ihren Bereich verlässt. Genau dies zeigt Listing 4. Am Ende der Funktion »f()« führt es die Lambda-Funktion »[p] { free(p); }« automatisch aus. Diese gibt in dem vorliegenden Fall den Speicher wieder frei.
Listing 4
finally als letzte Rettung
01 void f(int n)
02 {
03 void* p = malloc(1, n);
04 auto _ = finally([p] { free(p); });
05 [...]
06 }
Concepts
Concepts sind ein Typsystem für Templates. Mit seiner Hilfe lassen sich Anforderungen an Templates stellen, die der Compiler dann verifiziert. Listing 5 zeigt Concepts in Aktion. Der generische Algorithmus »gcd« (Zeilen 9 bis 15) soll im konkreten Fall den größten gemeinsamen Teiler (Greatest Common Divisor, GCD) zweier Zahlen nach dem bekannten Euklid-Algorithmus [9] bestimmen. Der Algorithmus setzt voraus, dass die Argumente ganze Zahlen sind. Dies drückt auch der Name des Typparameters »Integral« in Zeile 9 aus.
Listing 5
Das Concept Integral
01 #include <type_traits>
02 #include <iostream>
03
04 template<typename T>
05 concept bool Integral(){
06 return std::is_integral<T>::value;
07 }
08
09 template<Integral T>
10 T gcd(T a, T b){
11 if( b == 0 ){ return a; }
12 else{
13 return gcd(b, a % b);
14 }
15 }
16
17 int main(){
18
19 std::cout << std::endl;
20
21 std::cout << "gcd(100, 10)= " << gcd(100, 10) << std::endl;
22
23 auto res = gcd(121, 11);
24 std::cout << "gcd(121, 11): " << res << std::endl;
25
26 Integral res2 = gcd(33, 99);
27 std::cout << "gcd(33, 99): " << res2 << std::endl;
28
29 std::cout << std::endl;
30
31 // auto res3 = gcd(1.5, 2.5);
32
33 }
Die Definition des Concept in den Zeilen 4 bis 7 wendet dazu die Funktion »std::is_integral()« der Type-Traits-Bibliothek [10] an, die zur Übersetzungszeit prüft, ob der Typparameter »T« eine Ganzzahl ist. Entwickler dürfen Concepts überall dort verwenden, wo auch »auto« zum Einsatz kommt. Dies ist neben der bereits vorgestellten Deklaration des Funktions-Template (Zeile 9) auch »res2« (Zeile 26) als Rückgabewert des Funktionsaufrufs »gcd(33, 99)«.
In diesem Zusammenhang bezeichnet »auto« einen uneingeschränkten und »Integral« einen eingeschränkten Platzhalter. Wer Concepts für ein neues Konzept hält, irrt sich. Sie lassen sich bereits jetzt mit einem aktuellen GCC verwenden (Abbildung 1) und dürften mit hoher Wahrscheinlichkeit eines der neuen Features von C++20 werden.
Was passiert, wenn der Entwickler den Ausdruck »auto res3 = gcd(1.5, 2.5)« in Zeile 31 einkommentiert? Der Compiler moniert dann, wie Abbildung 2 deutlich zeigt, dass keine Zusicherungen an den Algorithmus gegeben sind.
Natürlich wäre es nicht nötig gewesen, das Concept »Integral« selbst zu definieren. C++20 bringt bereits von Haus aus einige Concepts mit. Die Abbildung 3 gibt einen ersten Überblick. Als sicher darf gelten: Concepts werden das Programmieren mit Templates in C++20 revolutionieren. Einige Gründe dafür:
- Entwickler dürfen die Anforderungen an die Templates als Teil des Interface formulieren.
- Aufgrund der Anforderungen (Concepts) an die Templates dürfen Entwickler Funktionen überladen und Klassen-Templates spezialisieren.
- Der Compiler erzeugt deutlich bessere Fehlermeldungen, da er Anforderungen an die Template-Parameter mit aktuellen Template-Argumenten prüft.
- Entwickler dürfen eigene Concepts definieren und verfeinern.
- C++ wird die Syntax der automatischen Typableitung mit »auto« und Concepts vereinheitlichen.
- Verwendet der Entwickler ein Concept wie »Integral« als Parameter einer Funktion, verwandelt diese sich automatisch in ein Funktions-Template. Das erfordert, dass seine Argumente das Concept »Integral« unterstützen.
Zum Abschluss gibt Listing 6 noch einen Vorgeschmack auf die Wirksamkeit von Concepts. Der Codeschnipsel definiert darin zusätzlich die Concepts »SignedIntegral()« und »UnsignedIntegral()«. Listing 7 interpretiert den »gcd«-Algorithmus aus Listing 5 deutlich kompakter – aber nur für natürliche Zahlen.
Listing 6
Verschiedene Concepts im Einsatz
01 template <class T>
02 concept bool Integral() {
03 return is_integral<T>::value;
04 }
05
06 template <class T>
07 concept bool SignedIntegral() {
08 return Integral<T>() && is_signed<T>::value;
09 }
10
11 template <class T>
12 concept bool UnsignedIntegral() {
13 return Integral<T>() && !SignedIntegral<T>();
14 }
Listing 7
Alternative Definition des Template gcd()
01 SignedIntegral gcd(SignedIntegral a, SignedIntegral b){
02 if( b == 0 ){ return a; }
03 else{
04 return gcd(b, a % b);
05 }
06 }
Weitere Details zu den Umwälzungen mit C++20 erläutert ein Blogpost zum Thema Concepts [11].
Wie geht’s weiter?
Funktionen sind das Mittel der Wahl, um große und komplexe Aufgaben in kleine, einfache Teilschritte zu zerlegen. Schritte, in denen die C++ Core Guidelines eine Menge zu sagen haben.
Infos
-
Guideline Support Library: http://http//isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#S-gsl
-
Microsofts GSL: http://https//github.com/Microsoft/GSL
-
Decay: http://https//en.cppreference.com/w/cpp/language/array#Array-to-pointer_decay
-
Off-by-one-Error: http://https//en.wikipedia.org/wiki/Off-by-one_error
-
Rainer Grimm, “Räumkommando”: Linux-Magazin 02/13, S. 90, https://www.linux-magazin.de/ausgaben/2013/02/c-11/
-
Rainer Grimm, “Klug aufgeräumt”: Linux-Magazin 04/13, S. 90, https://www.linux-magazin.de/ausgaben/2013/04/c-11/
-
Rainer Grimm, “Gut verträglich”: Linux-Magazin 02/19, S. 86, https://www.linux-magazin.de/ausgaben/2019/02/c-erfolgsrezepte-3/
-
Algorithmus zum größten gemeinsamen Teiler: https://de.wikipedia.org/wiki/Gr%C3%B6%C3%9Fter_gemeinsamer_Teiler
-
Type-Traits-Bibliothek: https://en.cppreference.com/w/cpp/header/type_traits
-
Blogpost zu Concepts: https://www.grimm-jaud.de/index.php/blog/tag/concepts









