Récemment, la communauté C++ encourage l'utilisation des nouvelles normes pour moderniser les bases de code existantes. Cependant, avant même la sortie de la norme C++11, des experts C++ reconnus comme Andrei Alexandrescu, Scott Meyers et Herb Sutter promouvaient déjà la programmation générique sous la bannière du Modern C++ Design. Voici ce que dit Andrei Alexandrescu à propos du Modern C++ Design :
Modern C++ Design définit et utilise systématiquement des des composants génériques. — des artefacts de conception hautement flexibles, combinables entre eux pour obtenir des comportements riches avec un corps de code petit et orthogonal.
Trois points de sa déclaration ressortent :
- Modern C++ Design définit et utilise systématiquement des composants génériques..
- Une conception hautement flexible.
- Obtenir des comportements riches avec un corps de code petit et orthogonal.
Moderniser votre code C++ ne consiste pas seulement à utiliser les nouvelles normes ; nous pouvons aussi appliquer certaines bonnes pratiques de la programmation générique pour améliorer notre base de code. Découvrons d'abord quelques étapes simples pour moderniser manuellement notre base de code, et dans la seconde partie nous verrons comment la moderniser automatiquement.
I - Moderniser votre code source manuellement
Prenons un algorithme comme exemple et essayons de le moderniser. Les algorithmes servent au calcul, au traitement de données et au raisonnement automatisé. Les programmer n'est pas toujours une tâche facile, et cela dépend de leur complexité. En C++, des efforts considérables ont été faits pour simplifier leur implémentation et les rendre plus puissants.
Essayons de moderniser cette implémentation de l'algorithme quicksort :
// 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);
}
}À haut niveau, voici quelques traits communs des algorithmes :
- L'utilisation de conteneurs d'un type d'élément donné et l'itération sur ceux-ci.
- La comparaison entre éléments.
- Et bien sûr, un certain traitement des éléments.
Dans notre implémentation, le conteneur est un tableau brut d'int ; nous itérons en incrémentant et en décrémentant. Nous comparons avec « < » et « > », et nous avons des opérations comme l'échange de données.
Essayons d'améliorer chacun de ces aspects :
Étape 1 : Remplacer les conteneurs par des itérateurs
L'utilisation de conteneurs non génériques nous oblige à utiliser un type d'élément spécifique. Pour appliquer le même algorithme à d'autres types, nous devons copier-coller le code. Les conteneurs génériques résolvent ce problème en permettant d'utiliser n'importe quel type d'élément ; par exemple, pour notre algorithme quicksort, nous pouvons utiliser std::vector<T> comme conteneur au lieu d'un tableau brut.
Un tableau brut ou un std::vector n'est qu'une possibilité parmi d'autres pour représenter un ensemble d'éléments ; nous pourrions aussi appliquer le même algorithme à une liste chaînée, une file ou tout autre conteneur. Pour cela, les itérateurs sont le meilleur choix pour faire abstraction du conteneur sous-jacent.
Un itérateur est un objet qui pointe vers un élément d'une plage et peut parcourir les éléments de cette plage à l'aide d'un ensemble d'opérateurs (avec au minimum les opérateurs d'incrément (++) et de déréférencement (*)). Les itérateurs sont classés en cinq catégories selon les fonctionnalités qu'ils implémentent : Input, Output, Forward, Bidirectional et Random Access.
Dans notre algorithme, nous devons préciser quel type d'itérateur utiliser. Pour cela, nous devons identifier quelles opérations d'itération sont utilisées. Pour l'algorithme quicksort, l'incrément et le décrément sont utilisés. Un itérateur bidirectionnel est donc requis. Avec les itérateurs, nous pouvons définir la méthode comme ceci :
template< typename BidirectionalIterator >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last )Étape 2 : Rendre le comparateur générique si possible
Pour certains algorithmes, les éléments traités ne sont pas nécessairement des nombres ; ce pourraient être des chaînes ou des objets de classes. Dans ce cas, rendre le comparateur générique nous donne un algorithme plus réutilisable.
L'algorithme quicksort pourrait aussi s'appliquer à une liste de chaînes ; il vaut donc mieux utiliser un comparateur générique.
Après l'utilisation d'un comparateur générique, la définition pourrait être modifiée comme ceci :
template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp )Étape 3 : Remplacer les opérations personnalisées par des opérations standard
De nombreux algorithmes utilisent des opérations récurrentes comme min, max et swap. Pour ces opérations, il vaut mieux ne pas réinventer la roue et utiliser les implémentations standard de l'en-tête <algorithm>.
Dans notre cas, nous pouvons utiliser la méthode swap de la STL plutôt que de créer notre propre méthode spécifique.
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 :
- Elle peut être appliquée à de nombreux types d'éléments.
- Le conteneur peut être un vector, un set, une list, ou tout autre conteneur doté d'un itérateur bidirectionnel.
- Elle utilise des fonctions standard bien optimisées et testées.
II - La modernisation automatique
Il est utile de détecter automatiquement les endroits où les fonctionnalités C++11/C++14/C++17 peuvent être utilisées et, si possible, de modifier le code automatiquement. Pour de tels besoins, clang-tidy est un outil autonome utilisé pour convertir automatiquement le code C++ écrit selon les anciennes normes afin qu'il utilise les fonctionnalités de la norme C++ la plus récente lorsque c'est approprié.
Voici quelques cas où clang-tidy peut détecter des opportunités de moderniser le code :
- Override : détecte les endroits où vous pouvez ajouter le spécificateur override aux fonctions membres qui redéfinissent une fonction virtuelle d'une classe de base et qui n'ont pas encore ce spécificateur.
- Loop Convert : détecte les boucles de type for(…; …; …) pour les remplacer par les nouvelles boucles basées sur les plages de C++11 et fournit l'expression de boucle basée sur les plages correspondante.
- Pass-By-Value : détecte les paramètres const-ref qui bénéficieraient de l'idiome du passage par valeur.
- auto_ptr : détecte les utilisations du std::auto_ptr déprécié pour les remplacer par std::unique_ptr.
- Spécificateur auto : détecte les endroits où le spécificateur de type auto peut être utilisé dans les déclarations de variables.
- nullptr : détecte les littéraux nuls à remplacer par nullptr lorsque c'est applicable.
- std::bind : cette vérification trouve les utilisations de
std::bindet remplace les cas simples par des lambdas. Les lambdas utiliseront la capture par valeur lorsque nécessaire. - En-têtes dépréciés : certains en-têtes de la bibliothèque C ont été dépréciés en C++ et ne sont plus les bienvenus dans les bases de code C++. Certains n'ont aucun effet en C++. Pour plus de détails, référez-vous à la section [depr.c.headers] de la norme C++14.
- std::shared_ptr : cette vérification trouve la création de
std::shared_ptrobjets en appelant explicitement le constructeur et unnewexpression, et la remplace par un appel àstd::make_shared. - std::unique_ptr : cette vérification trouve la création de
std::unique_ptrobjets en appelant explicitement le constructeur et unnewexpression, et la remplace par un appel àstd::make_unique, introduit en C++14. - Littéraux de chaînes brutes : cette vérification remplace sélectivement les littéraux de chaînes contenant des caractères échappés par des littéraux de chaînes brutes (raw string literals).
Les développeurs qui utilisent Clang peuvent facilement profiter de clang-tidy. Les développeurs Visual C++ et les utilisateurs d'autres compilateurs peuvent utiliser CppDepend, qui intègre clang-tidy.
