La bibliothèque Ranges en C++20 : décisions de conception

La bibliothèque Ranges en C++20 : décisions de conception

2023-10-30 11:16:00

Grâce à la bibliothèque Ranges, travailler avec la bibliothèque de modèles standard (STL) est devenu beaucoup plus pratique et puissant. Tout d’abord, les algorithmes de la bibliothèque Ranges sont paresseux, peuvent fonctionner directement sur le conteneur et sont composables. De plus, la bibliothèque Ranges a pris quelques décisions de conception spéciales dont il est important de tenir compte.

Publicité


Rainer Grimm travaille depuis de nombreuses années en tant qu’architecte logiciel, responsable d’équipe et de formation. Il aime écrire des articles sur les langages de programmation C++, Python et Haskell, mais aime également intervenir fréquemment lors de conférences spécialisées. Sur son blog Modern C++, il parle intensément de sa passion C++.



Avant d’entrer plus en détail sur la bibliothèque Ranges en C++20, j’aimerais résumer les trois fonctionnalités les plus importantes des Ranges en quelques phrases : Les algorithmes Ranges peuvent opérer directement sur le conteneur, évaluer leurs arguments si nécessaire et ils peuvent être composés.

La bibliothèque Ranges permet à un conteneur comme std::ranges::sort peut intervenir directement sur le conteneur :

// sortRanges.cpp

#include 
#include 
#include 

int main()  {

    std::vector myVec{-3, 5, 0, 7, -4};
    std::ranges::sort(myVec);                  // (1)
    for (auto v: myVec) std::cout << v << " "; // -4, -3, 0, 5, 7

}

En revanche, la méthode classique fonctionne std::sort sur une plage définie par deux itérateurs : std:sort(myVec.begin(), myVec.end()).

Les algorithmes Ranges sont paresseux et peuvent être composés.

Le programme suivant primesLazy.cpp applique les deux fonctionnalités. Il génère les dix premiers nombres premiers commençant par un million.

// primesLazy.cpp

#include 
#include 


bool isPrime(int i) {
    for (int j=2; j*j <= i; ++j){
        if (i % j == 0) return false;
    }
    return true;
}

int main() {
                                        
    std::cout << 'n';
                                         
    auto odd = [](int i){ return i % 2 == 1; };

    for (int i: std::views::iota(1'000'000) | std::views::filter(odd) 
                                            | std::views::filter(isPrime) 
                                            | std::views::take(10)) {
        std::cout << i << " ";  
    }

    std::cout << 'n';

}

La composition des fonctions peut être lue de gauche à droite : je crée un flux infini de données en utilisant 1'000'000 commence (std::views::iota(1'000'000)) et appliquez deux filtres. Chaque filtre a besoin d'un prédicat. Le premier filtre laisse passer les éléments impairs (std::views::filter(odd)), et le deuxième filtre laisse passer les nombres premiers (std::views::filter(isPrime)). Après dix numéros (std::views::take(10)) Je mets fin au flux de données infini. Enfin, voici les dix premiers nombres premiers commençant par un million.



Cela soulève la question de savoir qui commence à traiter ce pipeline de données. Eh bien, ça va de droite à gauche. Le récepteur de données (std::views::take(10)) veut la valeur suivante et interroge son prédécesseur. Cette requête se poursuit jusqu'à ce que la boucle for basée sur la plage renvoie la valeur suivante. La boucle peut produire un flux infini de données, mais ne le fait que sur demande. C’est une évaluation paresseuse.

C'était mon bref résumé. Si vous souhaitez en savoir plus sur la bibliothèque Ranges, notamment Sentinels, Projection et Concepts, consultez mes articles précédents :

  1. La bibliothèque des gammes
  2. Modèles fonctionnels avec la bibliothèque Ranges
  3. La bibliothèque Ranges en C++20 : Plus de détails
  4. Projections avec plages
  5. Sentinelles et concepts avec portées
  6. Itérateurs améliorés avec des plages
  7. Utilisation pythonique de la bibliothèque Ranges : plage et filtre
  8. Fonction range de Python, la seconde
  9. Fonction de carte de Python

Mais maintenant, je veux écrire sur quelque chose de nouveau.

Pour plus d'efficacité, la bibliothèque Ranges a pris des décisions de conception uniques. Il est important de les connaître et de les suivre.

qui le begin-Fonction de membre de std::ranges::filter_view étudié, trouve un code qui correspond à ce qui suit :

if constexpr (!ranges::forward_range)
    return /* iterator */{*this, ranges::find_if(base_, std::ref(*pred_))};
else
{
    if (!begin_.has_value())
        begin_ = ranges::find_if(base_, std::ref(*pred_)); // caching
    return /* iterator */{*this, begin_.value())};
}

Celui imbriqué if-La structure mérite une analyse : le compilateur vérifie d'abord si begin_.has_value() true est. Sinon, il décide begin_. Cela signifie que cette fonction membre renvoie le résultat dans le std::ranges::filter_viewL'objet est mis en cache pour être utilisé lors d'appels ultérieurs. Cette mise en cache a des conséquences intéressantes. Je voudrais illustrer cela à l’aide d’un extrait de code.

// cachingRanges.cpp

#include 
#include 
#include 
#include 
 
int main() {

    std::vector vec(1'000'000);
    std::iota(vec.begin(), vec.end(), 0);

    for (int i: vec | std::views::filter([](auto v) { return v > 1000; }) 
                    | std::views::take(5)) {
                        std::cout << i << " ";  // 1001 1002 1003 1004 1005
                    }
    
}

Le premier appel de std::views::filter([](auto v) { return v > 1000; }) détermine l'itérateur de départ et le réutilise dans les appels suivants. L’avantage de cette mise en cache est évident : de nombreuses itérations ultérieures du pipeline sont évitées. Mais il existe également de sérieux inconvénients : des problèmes de cache et des problèmes de cohérence.

Voici les deux règles de mise en cache les plus importantes pour les plages :

  • N'utilisez pas de vue pour une plage modifiée !
  • Ne copiez pas une vue !

Le changement suivant par rapport au programme précédent cachingRanges.cpp enfreint les deux règles :

// cachingIssuesRanges.cpp

#include 
#include 
#include 
#include 
#include 
#include 

void printElements(std::ranges::input_range auto&& rang) {
    for (int i: rang) {
        std::cout << i << " ";  
    }
    std::cout << 'n';
}
 
int main() {

    std::cout << 'n';

    std::vector vec{-3, 10, 4, -7, 9, 0, 5, -5};                  // (1)
    std::forward_list forL{-3, 10, 4, -7, 9, 0, 5, -5};           // (2)

    auto first5Vector = vec | std::views::filter([](auto v) { return v > 0; })   // (3)
                            | std::views::take(5);

    auto first5ForList = forL | std::views::filter([](auto v) { return v > 0; }) // (4) 
                              | std::views::take(5);            

    printElements(first5Vector);           // 10 4 9 5                  // (5)
    printElements(first5ForList);          // 10 4 9 5                  // (6)
    
    std::cout << 'n';

    vec.insert(vec.begin(), 10);
    forL.insert_after(forL.before_begin(), 10);


    printElements(first5Vector);           // -3 10 4 9 5
    printElements(first5ForList);          // 10 4 9 5

    std::cout << 'n';

    auto first5VectorCopy{first5Vector};                                 // (7)
    auto first5ForListCopy{first5ForList};                               // (8)

    printElements(first5VectorCopy);       // -3 10 4 9 5
    printElements(first5ForListCopy);      // 10 10 4 9 5
    
    std::cout << 'n';

}

Pour mieux comprendre le problème, j'ai écrit le résultat directement dans le code source. Le programme effectue les étapes suivantes avec un std::vector et une std::forward_list à travers. Tout d'abord, les deux conteneurs sont initialisés avec la liste d'initialisation {-3, 10, 4, -7, 9, 0, 5, -5} initialise (1) et (2). Ensuite je crée deux vues (3) et (4). Les deux vues first5Vector et first5ForList se composent des cinq premiers éléments supérieurs à 0. Les valeurs correspondantes sont affichées en (5) et (6).

Maintenant, j'enfreins la première règle : "Ne pas utiliser de vue sur les zones modifiées". J'ajoute le numéro au début des deux conteneurs 10 un. Puis montre first5Vector mourir -3 sur et first5ForList ignore le 10 inséré. Après avoir enfreint la deuxième règle "Ne pas copier une vue" dans (7) et (8), le cache de first5ForListCopy invalide. first5VectorCopy affiche toujours les mauvais chiffres. Enfin, voici le résultat du programme.



Voici une règle simple : Utilisez les vues immédiatement après les avoir définies!

La fonction printElements reçoit ses arguments via une référence universelle, également appelée référence de transfert.

Dans mon prochain article, j'écrirai pourquoi une fonction doit accepter n'importe quelle vue via une référence universelle.

J'ai le regret de vous informer que je souffre de SLA, une maladie nerveuse évolutive très grave. Je ne sais donc pas combien de temps je pourrai continuer ce blog. Actuellement, je ne peux voyager pour des formations ou des conférences qu'avec l'aide de ma femme. Nous (ma famille et moi) avons décidé de relever ce défi de manière agressive. Il était donc important pour moi de vous faire part, vous, mes fidèles lecteurs, de ma maladie.


(moi)

Vers la page d'accueil



#bibliothèque #Ranges #C20 #décisions #conception
1698886581

Facebook
Twitter
LinkedIn
Pinterest

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.