In jüngster Zeit wirbt die C++-Community verstärkt dafür, bestehende Codebasen mithilfe der neuen Standards zu modernisieren. Doch schon vor der Veröffentlichung von C++11 propagierten bekannte C++-Experten wie Andrei Alexandrescu, Scott Meyers und Herb Sutter generische Programmierung unter dem Begriff „Modern C++ Design“. Andrei Alexandrescu beschreibt Modern C++ Design folgendermaßen:
Modern C++ Design definiert und verwendet systematisch generische Komponenten – hochflexible Designbausteine, die sich kombinieren lassen, um mit einer kleinen, orthogonalen Codebasis ein umfangreiches Verhalten zu erzielen.
Drei Punkte seiner Aussage stechen besonders hervor:
- Modern C++ Design definiert und verwendet systematisch generische Komponenten.
- Hochgradig flexibles Design.
- Umfangreiches Verhalten mit einer kleinen, orthogonalen Codebasis erzielen.
Bei der Modernisierung von C++-Code geht es nicht nur darum, neue Standards einzusetzen. Auch bewährte Praktiken der generischen Programmierung können unsere Codebasis verbessern. Zunächst sehen wir uns einige einfache Schritte zur manuellen Modernisierung an; im zweiten Teil betrachten wir, wie sich die Modernisierung automatisieren lässt.
I – Quellcode manuell modernisieren
Nehmen wir einen Algorithmus als Beispiel und versuchen wir, ihn zu modernisieren. Algorithmen werden für Berechnungen, Datenverarbeitung und automatisierte Schlussfolgerungen eingesetzt. Ihre Implementierung ist je nach Komplexität nicht immer einfach. In C++ wurde viel unternommen, um ihre Umsetzung zu vereinfachen und zugleich leistungsfähiger zu machen.
Versuchen wir, diese Implementierung des Quicksort-Algorithmus zu modernisieren:
// The partition function
int partition(int* input,int p,int r){
int pivot = input[r];
while( p < r ){
while( input[p]< pivot )
p++;
while( input[r]> pivot )
r--;
if( input[p]== input[r])
p++;
elseif( p < r ){
int tmp = input[p];
input[p]= input[r];
input[r]= tmp;
}
}
return r;
}
// The quicksort recursive function
void quicksort(int* input,int p,int r){
if( p < r ){
int j = partition(input, p, r);
quicksort(input, p, j-1);
quicksort(input, j+1, r);
}
}Auf hoher Ebene weisen Algorithmen häufig folgende Merkmale auf:
- Container eines bestimmten Elementtyps verwenden und über deren Elemente iterieren.
- Elemente miteinander vergleichen.
- Und natürlich die Elemente auf bestimmte Weise verarbeiten.
In unserer Implementierung ist der Container ein einfaches int-Array; wir iterieren durch Inkrementieren und Dekrementieren. Verglichen wird mit „<“ und „>“, außerdem führen wir Operationen wie das Vertauschen von Daten aus.
Versuchen wir, jeden dieser Aspekte zu verbessern:
Schritt 1: Container durch Iteratoren abstrahieren
Nicht-generische Container zwingen uns auf einen bestimmten Elementtyp fest. Soll derselbe Algorithmus mit anderen Typen arbeiten, müssten wir den Code kopieren und anpassen. Generische Container lösen dieses Problem, da sie beliebige Elementtypen ermöglichen. Für unseren Quicksort-Algorithmus können wir beispielsweise std::vector<T> anstelle eines einfachen Arrays verwenden.
Ein einfaches Array oder ein std::vector ist nur eine von vielen Möglichkeiten, eine Menge von Elementen darzustellen. Derselbe Algorithmus könnte auch auf eine verkettete Liste, eine Queue oder einen anderen Container angewendet werden. Iteratoren eignen sich hervorragend, um den zugrunde liegenden Container zu abstrahieren.
Ein Iterator ist ein Objekt, das auf ein Element innerhalb eines Bereichs zeigt und dessen Elemente mithilfe bestimmter Operatoren durchlaufen kann – mindestens mit dem Inkrementoperator (++) und dem Dereferenzierungsoperator (*). Je nach unterstützter Funktionalität werden Iteratoren in fünf Kategorien eingeteilt: Input, Output, Forward, Bidirectional und Random Access.
Für unseren Algorithmus müssen wir festlegen, welche Art von Iterator benötigt wird. Dazu betrachten wir die verwendeten Iterationsoperationen. Beim Quicksort-Algorithmus werden Inkrement und Dekrement eingesetzt; daher ist ein bidirektionaler Iterator erforderlich. Mit Iteratoren können wir die Methode wie folgt definieren:
template< typename BidirectionalIterator >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last )Schritt 2: Den Comparator möglichst generisch gestalten
Bei manchen Algorithmen sind die verarbeiteten Elemente nicht zwangsläufig Zahlen; es können auch Strings oder Klassenobjekte sein. Ein generischer Comparator macht den Algorithmus in solchen Fällen deutlich besser wiederverwendbar.
Der Quicksort-Algorithmus kann beispielsweise auch auf eine Liste von Strings angewendet werden. Deshalb ist ein generischer Comparator die bessere Wahl.
Mit einem generischen Comparator lässt sich die Definition wie folgt anpassen:
template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp )Schritt 3: Eigene Operationen durch Standardfunktionen ersetzen
Viele Algorithmen verwenden wiederkehrende Operationen wie min, max und swap. Hier sollte man das Rad nicht neu erfinden, sondern die Standardimplementierungen aus dem Header <algorithm> nutzen.
In unserem Fall können wir die swap-Funktion der STL verwenden, anstatt eine eigene Variante zu implementieren.
std::iter_swap( pivot, left );Nach diesen drei Schritten erhalten wir folgende überarbeitete Version:
#include <functional>
#include <algorithm>
#include <iterator>
template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp ) {
if( first != last ) {
BidirectionalIterator left = first;
BidirectionalIterator right = last;
BidirectionalIterator pivot = left++;
while( left != right ) {
if( cmp( *left, *pivot ) ) {
++left;
} else {
while( (left != right) && cmp( *pivot, *right ) )
--right;
std::iter_swap( left, right );
}
}
--left;
std::iter_swap( pivot, left );
quick_sort( first, left, cmp );
quick_sort( right, last, cmp );
}
}
template< typename BidirectionalIterator >
inline void quick_sort( BidirectionalIterator first, BidirectionalIterator last ) {
quick_sort( first, last,
std::less_equal< typename std::iterator_traits< BidirectionalIterator >::value_type >()
);
}Diese Implementierung bietet folgende Vorteile:
- Sie lässt sich auf viele verschiedene Elementtypen anwenden.
- Als Container können vector, set, list oder jeder andere Container mit bidirektionalem Iterator verwendet werden.
- Sie verwendet gut optimierte und erprobte Standardfunktionen.
II – Automatische Modernisierung
Es ist hilfreich, Stellen automatisch zu erkennen, an denen Funktionen aus C++11/C++14/C++17 eingesetzt werden können, und den Code nach Möglichkeit automatisch anzupassen. Für diesen Zweck ist clang-tidy ein eigenständiges Tool, das C++-Code auf Basis älterer Standards automatisch so umwandelt, dass an geeigneten Stellen Funktionen neuerer C++-Standards verwendet werden.
Hier einige Fälle, in denen clang-tidy Möglichkeiten zur Modernisierung des Codes erkennen kann:
- Override: Erkennt Memberfunktionen, die eine virtuelle Funktion einer Basisklasse überschreiben, aber noch nicht mit dem override-Spezifizierer versehen sind.
- Loop Convert: Erkennt Schleifen wie for(…; …; …), die durch die seit C++11 verfügbaren range-based for-Schleifen ersetzt werden können, und erzeugt den entsprechenden Ausdruck.
- Pass-By-Value: Erkennt const-Referenzparameter, bei denen das Pass-by-Value-Idiom vorteilhaft wäre.
- auto_ptr: Erkennt die Verwendung des veralteten std::auto_ptr und ersetzt ihn durch std::unique_ptr.
- auto-Spezifizierer: Erkennt Stellen, an denen der Typspezifizierer auto in Variablendeklarationen verwendet werden kann.
- nullptr: Erkennt Null-Literale, die gegebenenfalls durch nullptr ersetzt werden können.
- std::bind: Die Prüfung findet Verwendungen von
std::bindund ersetzt einfache Verwendungen durch Lambdas. Wo erforderlich, verwenden die Lambdas Value-Capture. - Veraltete Header: Einige Header der C-Bibliothek wurden in C++ als veraltet markiert und sollten in C++-Codebasen nicht mehr verwendet werden. Manche haben in C++ keine Wirkung. Weitere Details finden Sie im C++14-Standard im Abschnitt [depr.c.headers].
- std::shared_ptr: Diese Prüfung erkennt die Erzeugung von
std::shared_ptr-Objekten durch den expliziten Aufruf des Konstruktors zusammen mit einemnew-Ausdruck und ersetzt sie durch einen Aufruf vonstd::make_shared. - std::unique_ptr: Diese Prüfung erkennt die Erzeugung von
std::unique_ptr-Objekten durch den expliziten Aufruf des Konstruktors zusammen mit einemnew-Ausdruck und ersetzt sie durch einen Aufruf vonstd::make_unique, das mit C++14 eingeführt wurde. - Raw-String-Literale: Diese Prüfung ersetzt gezielt String-Literale mit Escape-Zeichen durch Raw-String-Literale.
Entwickler, die Clang verwenden, können clang-tidy direkt nutzen. Visual-C++-Entwickler und Anwender anderer Compiler können CppDepend, which integrates clang-tidy.
