Wie einfach es ist, C ++ - Code zu modernisieren

Hallo Habr!



Wir machen Sie auf die Übersetzung eines kurzen praktischen Artikels zum Umgang mit redundantem Erbe in C ++ - Code aufmerksam. Wir hoffen es wird interessant.



Vor kurzem hat die C ++ - Community die Verwendung neuer Standards und die Modernisierung der vorhandenen Codebasis aktiv gefördert. Bereits vor der Veröffentlichung des C ++ 11-Standards förderten renommierte C ++ - Experten wie Andre Alexandrescu, Scott Myers und Herb Sutter die generische C ++ - Programmierung, die sie als "modernes C ++ - Design" qualifizierten. André Alexandrescu drückte es so aus:



Modernes C ++ - Design definiert und verwendet systematisch generische Komponenten - hochflexible Designartefakte, die gemischt und angepasst werden können, um ein umfangreiches Verhalten in einem kleinen, orthogonalen Code zu erzeugen.


Drei Aussagen sind in dieser Arbeit interessant:



  • Modernes C ++ - Design definiert und verwendet systematisch generische Komponenten .
  • Sehr flexibles Design.
  • Erhalten Sie reichhaltiges Verhalten mit einem kleinen, orthogonalen Code.


Das Aktualisieren von in C ++ geschriebenem Code beschränkt sich nicht nur auf die Einführung neuer Standards, sondern umfasst auch die Verwendung von Best Practices, die für jede Programmiersprache gelten und zur Verbesserung der Codebasis beitragen. Lassen Sie uns zunächst einige einfache Schritte zum manuellen Aktualisieren Ihrer Codebasis erläutern. Im dritten Abschnitt werden wir über automatische Code-Upgrades sprechen.



Manuelles Aktualisieren des Quellcodes



Nehmen wir als Beispiel einen Algorithmus und versuchen, ihn zu modernisieren. Algorithmen werden zur Berechnung, Datenverarbeitung und automatischen Ableitung von Schlussfolgerungen verwendet. Das Programmieren eines Algorithmus ist manchmal eine nicht triviale Aufgabe und hängt von seiner Komplexität ab. In C ++ werden erhebliche Anstrengungen unternommen, um die Implementierung zu vereinfachen und die Leistung von Algorithmen zu erhöhen.

Versuchen wir, diese Implementierung des Quicksort-Algorithmus zu modernisieren:



//  
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;
}
//    
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);
        }
}

      
      





Schließlich haben alle Algorithmen bestimmte Gemeinsamkeiten:



  • Verwenden eines Containers für Elemente eines bestimmten Typs und Durchlaufen dieser Elemente.
  • Vergleich von Elementen
  • Einige Operationen an Elementen


In unserer Implementierung ist der Container ein unformatiertes Array von Ganzzahlen, und wir durchlaufen die Inkrementierungs- und Dekrementierungsoperationen um eins. Der Vergleich wird mit “<”



und durchgeführt “>”



, und wir führen auch einige Operationen an den Daten durch, z. B. tauschen Sie sie aus.



Versuchen wir, jede dieser Funktionen des Algorithmus zu verbessern:



Schritt 1: Ändern von Containern zu Iteratoren



Wenn wir generische Container aufgeben, müssen wir nur Elemente eines bestimmten Typs verwenden. Um denselben Algorithmus auf andere Typen anzuwenden, müssen wir den Code kopieren und einfügen. Generische Container lösen dieses Problem und ermöglichen die Verwendung beliebiger Elemente. In einem schnellen Sortieralgorithmus können Sie ihn std::vector<T>



beispielsweise als Container anstelle eines Raw-Arrays verwenden.



Ein Raw-Array oder std::vector



ist nur eine von vielen Optionen zur Darstellung vieler Elemente. Der gleiche Algorithmus gilt für eine verknüpfte Liste, eine Warteschlange oder einen anderen Container. Wenn Sie mit einem Iterator arbeiten, ist es am besten, den verwendeten Container zu abstrahieren.



Ein Iterator ist ein beliebiges Objekt, das auf ein Element in einem bestimmten Bereich zeigt und mit einer Reihe von Operatoren (die mindestens das Inkrement um einen Operator (++) und den Dereferenzierungsoperator (*) enthalten) über alle Elemente des angegebenen Bereichs iterieren kann. Iteratoren lassen sich je nach der von ihnen ausgeführten Funktion in fünf Kategorien einteilen: Eingabe, Ausgabe, Einweg-Iterator, Zweiweg-Iterator und Direktzugriff.



In unserem Algorithmus müssen wir angeben, welchen Iterator wir verwenden werden. Dazu müssen wir identifizieren, welche Iterationen wir verwenden. Der Quicksort-Algorithmus verwendet eine Iteration von Inkrement um eins und Dekrement um eins. Daher benötigen wir einen bidirektionalen Iterator. Mit Iteratoren können Sie eine Methode wie die folgende definieren:



template< typename BidirectionalIterator >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last )
      
      





Schritt 2: Verallgemeinern Sie den Komparator, wenn möglich



Einige Algorithmen müssen nicht nur Zahlen verarbeiten, sondern beispielsweise eine Zeichenfolge oder eine Klasse. In diesem Fall müssen Sie den Komparator verallgemeinern. Dies ermöglicht es uns, eine größere Verallgemeinerung des gesamten Algorithmus zu erreichen.



Der schnelle Sortieralgorithmus kann auch auf eine Liste von Zeichenfolgen angewendet werden. Dementsprechend ist ein verallgemeinerter Komparator für uns besser geeignet.



Mit dem generischen Komparator können Sie die Definition folgendermaßen ändern:



template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp )
      
      





Stufe 3: Ersetzen Sie vorhandene Vorgänge durch Standardvorgänge



Die meisten Algorithmen verwenden sich wiederholende Vorgänge wie min



, max



und swap



. Wenn Sie solche Vorgänge ausführen, ist es besser, das Rad nicht neu zu erfinden und die im Header vorhandene Standardimplementierung zu verwenden <algorithm>



.



In unserem Fall können wir die Swap-Methode aus der STL-Standardbibliothek verwenden, anstatt eine eigene Methode zu erstellen.



std::iter_swap( pivot, left );
      
      





Und hier ist das modifizierte Ergebnis nach diesen drei Schritten:



#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 hat folgende Vorteile:



  • Anwendbar auf alle Arten von Elementen.
  • Der Container kann ein Vektor, eine Menge, eine Liste oder ein anderer sein, der mit einem bidirektionalen Iterator versehen ist.
  • Diese Implementierung verwendet optimierte und getestete Standardfunktionen.


Automatisches Upgrade



Es ist interessant, Orte automatisch zu identifizieren, an denen Sie bestimmte Funktionen von C ++ 11 / C ++ 14 / C ++ 17 verwenden können, und bei günstigen Bedingungen den Code automatisch zu ändern. Für solche Zwecke gibt es ein voll funktionsfähige Klirren-ordentlich Werkzeug verwendet , um automatisch C ++ Code geschrieben nach alten Standards zu verwandeln. Nach dieser Umwandlung verwendet der Code gegebenenfalls Funktionen aus neueren Standards.



Hier sind einige Bereiche, in denen Clang-Tidy Code-Upgrades vorschlägt:



  • Überschreiben: Suchen Sie nach Stellen, an denen Sie einen Überschreibungszeiger für eine Instanzfunktion hinzufügen können, die eine virtuelle Funktion in der Basisklasse überschreibt, ohne bereits eine zu haben
  • : for(…; …; …)



    , , , .
  • : const-ref



    , .
  • auto_ptr



    : std::auto_ptr



    std::unique_ptr



    .
  • -: , auto



    .
  • nullptr



    : , nullptr



    , .
  • std::bind



    : std::bind , , . , , .
  • : C C++ . C++. C++ 14 [depr.c.headers].
  • std::shared_ptr



    : std::shared_ptr



    new



    , std::make_shared



    .
  • std::unique_ptr



    : std::shared_ptr



    new



    , std::make_unique



    , C++14.
  • : , , .


Entwickler, die Clang beherrschen, können leicht lernen, das Clang-Tidy-Tool zu verwenden. Wenn Sie jedoch mit Visual C ++ sowie mit anderen Compilern arbeiten, können Sie CppDepend verwenden , das Clang-Tidy enthält.



All Articles