C++ Core Guidelines: The Guideline Support Library

Modernes C++  –  1 Kommentare

Die Guideline Support Library (GSL) ist eine kleine Bibliothek, die die Regeln der C++ Core Guidelines unterstützt. Ihr Ziel ist es, Entwicklern zu helfen, besseren C++-Code zu schreiben. Daher geht es vor allem um Speicher- und Typsicherheit. Es gibt bereits Implementierungen der GSL.

Die GSL ist eine Bibliothek, die nur aus Headern besteht. Daher kannst du ihre Funktionen und Datentypen sehr leicht verwenden. Die bekannteste Implementierung ist von Microsoft und lässt sich direkt von GitHub herunterladen: Microsoft/GSL. Microsofts Implementierung setzt den C++14-Standard voraus und läuft auf vielen Plattformen. Hier sind die bekanntesten:

  • Windows mit Visual Studio 2015
  • Windows mit Visual Studio 2017
  • GNU/Linux mit Clang/LLVM 3.6
  • GNU/Linux mit GCC 5.1

Es gibt noch mehr Implementierungen auf GitHub. Ich will explizit die GSL-lite von Martin Moene erwähnen, die bereits die Standards C++98 und C++03 unterstützt.

Bevor ich in die Details abtauche, möchte ich einen Punkt erwähnen, der meinen Schreibfluss empfindlich stört: der Mangel an guter Dokumentation oder Tutorials. Um eine detaillierte Vorstellung zu erhalten, welche Absicht die Datentypen und Funktionen der GSL besitzen, musst du die Bibliothek installieren und die Unit-Tests analysieren. Das ist nicht die Art der Dokumentation, die ich erwarte. Im Gegensatz dazu war die Installation und das Verwenden von Microsofts Implementierung der GSL unter Windows und Linux sehr einfach.

So, nun geht es aber in die Details. Die GSL besteht aus fünf Komponenten. Hier kommt der erste Überblick:

  • GSL.view: Views
    • span<T>
    • string_span<T>
    • (cw)zstring
  • GSL.owner
    • owner<T>
    • unique_ptr<T>
    • shared_ptr<T>
    • dyn_array<T>
    • stack_array<T>
  • GSL.assert: Assertions
    • Expects()
    • Ensures()
  • GSL.util: Utilities
    • narrow
    • narrow_cast()
    • not_null<T>
    • finally
  • GSL.concept: Concepts
    • Range
    • String
    • Number
    • Sortable
    • Pointer
    • ...

Du wunderst dich vermutlich, dass die GSL ihre eigenen Smart Pointer gsl::unique_ptr und gsl::shared_ptr mitbringt, denn der C++11-Standard besitzt bereits std::unique_ptr und std::shared_ptr. Der Grund ist naheliegend: Du kannst die GSL bereits mit einem Compiler verwenden, der C++11 nicht unterstützt. Viele der Funkionen und Datentypen der GSL werden Bestandteil von C++20 werden. Das gilt zu mindestens für Concepts und Assertions. Darüber hinaus ist es sehr wahrscheinlich, dass die verbleibenden Komponenten in zukünftige C++-Standards aufgenommen werden.

Die Komponenten

Los geht es mit dem Views:

GSL.view: Views

Eine View ist niemals ein Besitzer (owner). Im Falle von gsl::span<T> repräsentiert sie einen Bereich von zusammenhängendem Speicher, ohne dessen Owner zu sein. Dabei kann es sich um einen Array, einen Zeiger mit seiner Länge oder einen std::vector handeln. Das Gleiche gilt für gsl::string_span<T> oder die Null-terminierten C-Strings: gsl::(cw)zstring. Der Grund für gsl::span<T> ist, dass ein einfaches Array zu einem Zeiger wird, falls dieses an eine Funktion übergeben wird. Damit geht zwangsläufig seine Länge verloren.

gsl::span<T> bestimmt automatisch die Länge eines Arrays oder seines Vektors. Falls du einen Zeiger verwendest, musst du die Länge explizit angeben.

template <typename T>
void copy_n(const T* p, T* q, int n){}

template <typename T>
void copy(gsl::span<const T> src, gsl::span<T> des){}

int main(){

int arr1[] = {1, 2, 3};
int arr2[] = {3, 4, 5};

copy_n(arr1, arr2, 3); // (1)
copy(arr1, arr2); // (2)

}

Im Gegensatz zu der Funktion copy_n (1) musst du bei der Funktion copy (2) nicht die Anzahl seiner Elemente angeben. Daher verschwindet mit gsl::span<T> ein häufiger Fehler von C- oder C++-Programmen.

Es gibt viele Typen von Besitzern (owner) in der GSL.

GSL.owner: Ownership pointers

Ich nehme an, du kennst bereits std::unique_ptr und std::shared_ptr und somit auch gsl::unique_ptr und gsl::shared_ptr. Falls dies nicht der Fall ist, sind hier meine Artikel über Smart Pointer in C++.

gsl::owner<T> ist ein Zeiger, der Besitzer seiner referenzierten Ressource ist. Du solltest gsl::owner<T> dann verwenden, wenn du keinen Resource Handle wie Smart Pointer oder Container einsetzen kannst. Der entscheidende Punkt eines Besitzers ist, dass du seine Ressource explizit freigeben musst. Direkte Zeiger (raw pointer), die nicht als gsl::owner<T> deklariert werden, gelten in den C++ Core Guidelines als Nicht-Besitzer. Daher bist du nicht in der Verantwortung, ihre Ressource freizugeben.

gsl::dyn_array<T> und gsl::stack_array<T> sind zwei neue Array-Typen.

  • gsl::dyn_array<T> ist ein Array fester Länge, das auf dem Heap angelegt wird. Seine Länge wird zur Laufzeit angeben.
  • gsl::stack_array<T> ist ein Array fester Länge, das auf dem Stack angelegt wird. Seine Länge wird zur Laufzeit angegeben.
GSL.assert: Assertions

Dank Expects() und Ensures() kannst du Vor- und Nachbedingungen an deine Funktionen stellen. Zum jetzigen Zeitpunkt musst du diese im Funktionskörper platzieren, aber später werden sie direkt in der Funktionsdeklaration spezifiziert. Beide Funktionen sind Bestandteil des Contract Proposal für C++20.

Hier ist ein einfaches Beispiel zur Verwendung von Expects() und Ensures().

int area(int height, int width)
{
Expects(height > 0);
auto res = height * width;
Ensures(res > 0);
return res;
}
GSL.util: Utilities

gsl::narrow_cast<T> und gsl::narrow sind zwei neue Konvertierungen.

  • gsl::narrow_cast<T> ist ein static_cast<T>, der nur seine Intention explizit ausdrückt. Eine verengende Konvertierung (Narrowing Conversion) ist möglich. Der Begriff bezeichnet eine Konvertierung mit Verlust der Datengenauigkeit.
  • gsl::narrow ist ein static_cast<T>, die eine narrowing_error-Ausnahme wirft, falls gilt, dass static_cast<T> != x ist.

gsl::not_null<T*> steht für einen Zeiger, der kein Nullzeiger (nullptr) sein kann. Falls du versuchst, einen gsl::not_null<T*>-Zeiger auf nullptr zu setzen, erhältst du einen Compilerfehler. Du kannst selbst einen Smart Pointer wie std::unique_ptr oder std::shared_ptr in einem gsl::not_null<T*> verwenden. Typischerweise verwendest du gsl::not_null<T*> für die Funktionsparameter und den Rückgabetyp einer Funktion. Daher kann es dir nicht passieren, dass du vergisst zu prüfen, ob der Zeiger tatsächlich einen Wert besitzt.

int getLength(gsl::not_null<const char*> p); // p cannot be a nullptr

int getLength(const char* p); // p can be a nullptr

Beide Funktionen bringen ihre Intention direkt auf den Punkt. Die zweite Funktion nimmt auch einen nullptr an.

finally erlaubt dir eine Funktion zu registrieren, die dann automatisch ausgeführt wird, wenn der Bereich (scope) verlassen wird.

void f(int n)
{
void* p = malloc(1, n);
auto _ = finally([p] { free(p); });
...
}

Am Ende der Funktion f wird die Lambda-Funktion [p] { free(p); } automatisch ausgeführt.

Gemäß den C++ Core Guidelines solltest du finally nur als letztes Hilfsmittel verwenden, wenn ein angemessenerer Umgang mit Ressourcen mittels Smart Pointer oder Container nicht möglich ist.

GSL.concept: Concepts

Ich halte mich sehr kurz, denn die meisten der Konzepte sind im Ranges TS bereits implementiert. TS steht für Technical Specification und damit eine Erweiterung für einen zukünftigen C++-Standard (C++20). Hier sind meine Artikel zu Concepts.

Meine letzten Worte

Mich hat die GSL sehr überzeugt. Was ich besonders an der Bibliothek schätzte, ist, dass sie keinen zu C++11 konformen Compiler benötigt. Du kannst sie mit deiner bestehenden Codebasis verwenden und dadurch deren Speicher- und Typsicherheit deutlich verbessern.

Ich habe fast vergessen, einen wichtigen Punkt zu erwähnen: die GSL "aims for zero-overhead when compared to equivalent hand-written checks". Wenn das keine Zusage ist!

Wie geht's weiter?

Nach meinem kleinen Umweg über die GSL werde ich zu den C++ Core Guidelines zurückkehren. Im nächsten Artikel geht es um Funktionen im Allgemeinen, ihre Parameter und ihre Rückgabewerte im Besonderen.