Maîtriser les algorithmes génériques en C++ : Principes et utilisation

Introduction aux algorithmes génériques

En C++, les conteneurs de la bibliothèque standard (STL) ne possèdent que peu de méthodes de manipulation directe. Au lieu d'intégrer des fonctions complexes comme le tri ou la recherche dans chaque type de conteneur, le langage utilise des algorithmes génériques. Le terme "générique" signifie que ces algorithmes sont indépendants du type de conteneur et travaillent sur une grande variété d'éléments.

Les algorithmes n'agissent jamais directement sur le conteneur lui-même, mais passent par des itérateurs. Cette abstraction permet d'appliquer la même logique de recherche à un std::vector, une std::list ou même un tableau brut.

Fonctionnement via les itérateurs

La plupart des algorithmes sont déclarés dans l'en-tête <algorithm>, tandis que les algorithmes numériques résident dans <numeric>. Voici un exemple simple avec std::find :

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

int main() {
    std::vector<std::string> mots = {"C++", "Python", "Rust", "Java"};
    std::string cible = "Rust";
    
    // find retourne un itérateur vers l'élément ou vers la fin si non trouvé
    auto it = std::find(mots.cbegin(), mots.cend(), cible);
    
    if (it != mots.cend()) {
        std::cout << cible << " a été trouvé dans la liste." << std::endl;
    } else {
        std::cout << cible << " est absent." << std::endl;
    }
    return 0;
}

Il est crucial de comprendre que les algorithmes ne modifient jamais la taile du conteneur. Ils peuvent modifier les valeurs ou déplacer les éléments, mais l'ajout ou la suppression de cellules mémoire nécessite l'appel explicite à des méthodes membres du conteneur (comme erase).

Algorithmes en lecture seule

Certains algorithmes se contentent de parcourir la séquence sans modifier les données. C'est le cas de std::accumulate et std::equal.

Sommation avec accumulate

L'algorithme accumulate (dans <numeric>) calcule la somme d'une plage d'éléments à partir d'une valeur initiale. Le type de cette valeur initiale détermine le type de retour.

#include <numeric>
#include <vector>

std::vector<double> mesures = {1.5, 2.5, 3.0};
// Le 0.0 indique un calcul en double
double total = std::accumulate(mesures.begin(), mesures.end(), 0.0);

Comparaison avec equal

L'algorithme equal compare deux séquences. Il suppose par défaut que la seconde séquence est au moins aussi longue que la plage définie pour la première.

bool sont_identiques = std::equal(v1.begin(), v1.end(), v2.begin());

Modification des éléments et itérateurs d'insertion

Pour écrire des données, on utilise des fonctions comme fill ou copy. Cependant, si le conteneur est vide, une écriture directe provoquera une erreur. C'est ici qu'intervient std::back_inserter.

#include <iterator>
#include <vector>
#include <algorithm>

std::vector<int> source = {1, 2, 3, 4, 5};
std::vector<int> destination; // Vide

// back_inserter appelle push_back à chaque itération
std::copy(source.begin(), source.end(), std::back_inserter(destination));

Personnalisation via les prédicats et expressions Lambda

De nombreux algorithmes acceptent un prédicat, c'est-à-dire une fonction ou un objet appelable qui retourne un booléen pour guider l'algorithme.

Les expressions Lambda

Une lambda est une fonction anonyme définie localement. Sa structure est : [capture](paramètres) -> type_retour { corps }.

  • Capture par valeur [=] : Une copie des variables locales est créée.
  • Capture par référence [&] : La lambda travaille sur les variables originales.
#include <vector>
#include <algorithm>

int seuil = 10;
std::vector<int> nombres = {5, 12, 8, 15, 3};

// Compter les nombres supérieurs au seuil
auto nb = std::count_if(nombres.begin(), nombres.end(), [seuil](int n) {
    return n > seuil;
});

L'adaptateur std::bind

Pour transformer une fonction prenant plusieurs arguments en un objet appelable compatible avec un algorithme (ex: transformer une fonction binaire en unaire), on utilise std::bind :

using namespace std::placeholders;
auto superieur_a_10 = std::bind(std::greater<int>(), _1, 10);
bool resultat = superieur_a_10(15); // Retourne vrai

Itérateurs de flux (Stream Iterators)

Les itérateurs peuvent lier les algorithmes directement aux entrées/sorties (IO).

  • std::istream_iterator : Lit les données depuis un flux (ex: cin).
  • std::ostream_iterator : Écrit les données vers un flux (ex: cout).
#include <iterator>
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3, 4};
    std::ostream_iterator<int> out_it(std::cout, " | ");
    std::copy(v.begin(), v.end(), out_it); // Affiche: 1 | 2 | 3 | 4 | 
    return 0;
}

Classification des itérateurs

Les algorithmes exigent des capacités spécifiques de la part des itérateurs. On distingue 5 catégories :

Catégorie Capacités Conteneurs typiques
Entrée (Input) Lecture seule, passage unique istream_iterator
Sortie (Output) Écriture seule, passage unique ostream_iterator, back_inserter
Avant (Forward) Lecture/Écriture, passages multiples forward_list
Bidirectionnel Incrément et décrément list, set, map
Accès Aléatoire Arithmétique (+n), accès direct [n] vector, deque, array

Algorithmes spécifiques aux listes

Pour les types std::list et std::forward_list, il est préférable d'utiliser leurs méthodes membres (sort, merge, remove, reverse, unique) plutôt que les algorithmes génériques. Les versions génériques de sort nécessitent des itérateurs à accès aléatoire, ce que les listes ne fournissent pas.

La méthode splice est également unique aux listes, permettant de transférer des nœuds d'une liste à une autre de manière très performante, sans copie des données réelles.

Étiquettes: C++ STL algorithm iterators Lambda

Publié le 16 septembre à 14h55