Comment il est facile de moderniser le code C ++

Bonjour, Habr!



Nous attirons votre attention sur la traduction d'un court article pratique sur la gestion de l'héritage redondant dans le code C ++. Nous espérons que ce sera intéressant.



Récemment, la communauté C ++ a activement encouragé l'utilisation de nouvelles normes et la modernisation de la base de code existante. Cependant, avant même la publication de la norme C ++ 11, des experts renommés du C ++ tels que Andre Alexandrescu, Scott Myers et Herb Sutter ont promu la programmation C ++ générique, qu'ils qualifiaient de «conception C ++ moderne». Voici comment Andre Alexandrescu l'a dit:



La conception C ++ moderne définit et utilise systématiquement des composants génériques - des artefacts de conception très flexibles qui peuvent être mélangés et assortis pour produire des comportements riches dans un petit morceau de code orthogonal.


Trois affirmations sont intéressantes dans cette thèse:



  • La conception C ++ moderne définit et utilise systématiquement des composants génériques .
  • Conception très flexible .
  • Obtenez des comportements riches avec un petit morceau de code orthogonal .


La modernisation du code écrit en C ++ ne se limite pas à l'introduction de nouvelles normes, mais implique également l'utilisation des meilleures pratiques qui s'appliquent à tout langage de programmation pour aider à améliorer la base de code. Tout d'abord, discutons de quelques étapes simples pour mettre à niveau manuellement votre base de code. Dans la troisième section, nous parlerons des mises à niveau automatiques du code.



Mise à jour manuelle du code source



Prenons un algorithme comme exemple et essayons de le moderniser. Les algorithmes sont utilisés pour les calculs, le traitement des données et la dérivation automatique des conclusions. La programmation d'un algorithme est parfois une tâche non triviale et dépend de sa complexité. En C ++, des efforts importants sont faits pour simplifier l'implémentation et augmenter la puissance des algorithmes.

Essayons de moderniser cette implémentation de l'algorithme de tri rapide:



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

      
      





Après tout, tous les algorithmes ont certaines choses en commun:



  • Utilisation d'un conteneur pour les éléments d'un certain type et itération sur eux.
  • Comparaison d'éléments
  • Quelques opérations sur les éléments


Dans notre implémentation, le conteneur est un tableau brut d'entiers, et nous itérons sur les opérations d'incrémentation et de décrémentation de un. La comparaison est effectuée à l'aide de “<”



et “>”



, et nous effectuons également certaines opérations sur les données, par exemple, nous les échangeons.



Essayons d'améliorer chacune de ces fonctionnalités de l'algorithme:



Étape 1: Changer les conteneurs en itérateurs



Si nous abandonnons les conteneurs génériques, nous serons obligés de n'utiliser que des éléments d'un certain type. Pour appliquer le même algorithme à d'autres types, nous devrons copier et coller le code. Les conteneurs génériques résolvent ce problème et vous permettent d'utiliser n'importe quel type d'élément. Par exemple, dans un algorithme de tri rapide, vous pouvez l'utiliser std::vector<T>



comme conteneur au lieu d'un tableau brut.



Tableau brut ou std::vector



n'est qu'une des nombreuses options permettant de représenter de nombreux éléments. Le même algorithme s'applique à une liste liée, une file d'attente ou tout autre conteneur. Lorsque vous travaillez avec un itérateur, il est préférable d'abstraire le conteneur utilisé.



Un itérateur est tout objet qui, pointant vers un élément dans une certaine plage, peut itérer sur tous les éléments de la plage donnée en utilisant un ensemble d'opérateurs (qui comprend au moins l'incrément d'un opérateur (++) et l'opérateur de déréférence (*)). Les itérateurs se répartissent en cinq catégories en fonction de la fonction qu'ils exécutent: entrée, sortie, itérateur unidirectionnel, itérateur bidirectionnel et accès aléatoire.



Dans notre algorithme, nous devons spécifier quel itérateur nous utiliserons. Pour ce faire, nous devons identifier les itérations que nous utilisons. L'algorithme de tri rapide utilise une itération d'incrémentation de un et de décrémentation de un. Par conséquent, nous avons besoin d'un itérateur bidirectionnel. Avec les itérateurs, vous pouvez définir une méthode comme celle-ci:



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





Étape 2: généraliser le comparateur, si possible



Certains algorithmes doivent traiter non seulement des nombres, mais, par exemple, une chaîne ou une classe. Dans ce cas, vous devez généraliser le comparateur; cela nous permettra de généraliser davantage l'ensemble de l'algorithme.



L'algorithme de tri rapide peut également être appliqué à une liste de chaînes. En conséquence, un comparateur généralisé nous convient mieux.



En utilisant le comparateur généralisé, vous pouvez modifier la définition comme ceci:



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





Étape 3: Remplacer les opérations existantes par des opérations standard



La plupart des algorithmes utilisent des opérations répétitives telles que min



, max



et swap



. Lors de telles opérations, il vaut mieux ne pas réinventer la roue et utiliser l'implémentation standard qui existe dans l'en-tête <algorithm>



.



Dans notre cas, nous pouvons utiliser la méthode swap de la bibliothèque standard STL au lieu de créer notre propre méthode.



std::iter_swap( pivot, left );
      
      





Et voici le résultat modifié après ces trois étapes:



#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 >()
                );
    }
      
      





Cette implémentation présente les avantages suivants:



  • Applicable à toutes sortes d'éléments.
  • Le conteneur peut être un vecteur, un ensemble, une liste ou tout autre pourvu d'un itérateur bidirectionnel.
  • Cette implémentation utilise des fonctions standard optimisées et testées.


Mise à jour automatique



Il est intéressant d'identifier automatiquement les endroits où vous pouvez utiliser certaines fonctionnalités de C ++ 11 / C ++ 14 / C ++ 17 et, si les conditions sont favorables, changer automatiquement le code. À ces fins, il existe un outil complet de clang-tidy utilisé pour transformer automatiquement le code C ++ écrit selon les anciennes normes. Après cette transformation, le code utilise, le cas échéant, des fonctionnalités de normes plus récentes.



Voici quelques domaines dans lesquels clang-tidy suggère des mises à niveau du code:



  • Remplacement: recherchez les emplacements où vous pouvez ajouter un pointeur de remplacement pour une fonction d'instance qui remplace une fonction virtuelle dans la classe de base sans en avoir déjà une.
  • : 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.
  • : , , .


Les développeurs qui maîtrisent Clang peuvent facilement apprendre à utiliser l'outil clang-tidy. Mais lorsque vous travaillez avec Visual C ++, ainsi que d'autres compilateurs, vous pouvez utiliser CppDepend , qui inclut clang-tidy.



All Articles