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.
Directement sur le conteneur
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.
Évaluation paresseuse et composition fonctionnelle
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 :
- La bibliothèque des gammes
- Modèles fonctionnels avec la bibliothèque Ranges
- La bibliothèque Ranges en C++20 : Plus de détails
- Projections avec plages
- Sentinelles et concepts avec portées
- Itérateurs améliorés avec des plages
- Utilisation pythonique de la bibliothèque Ranges : plage et filtre
- Fonction range de Python, la seconde
- Fonction de carte de Python
Mais maintenant, je veux écrire sur quelque chose de nouveau.
Décisions de conception
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_view
L'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.
Cache
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.
Et après?
Dans mon prochain article, j'écrirai pourquoi une fonction doit accepter n'importe quelle vue via une référence universelle.
Voici une autre note en mon nom personnel
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)
#bibliothèque #Ranges #C20 #décisions #conception
1698886581