Salut les agrumes ! Bienvenue dans ce nouvel épisode de Causerie++ ! Pour l’épisode précédent, c’est ici que ça se passe !
Dans cet épisode, nous allons continuer notre petit tour d’horizon du C++20 avec les Ranges1 !
-
Si vous avez une idée de traduction, je suis preneur.
↩
[C++20] Ranges
Encore une fonctionnalité très alléchante. Les ranges sont un moyen de gagner encore en abstraction lors d’utilisation d’algorithmes sur les collections, en particulier les algorithmes de la STL.
En effet, jusqu’à maintenant, on donnait deux itérateurs pour délimiter la collection/sous-collection que l’on voulait traiter. Les ranges encapsulent cela dans un concept de plus haut niveau, ce qui a plusieurs intérêts.
Une interface plus naturelle
Nos appels d’algorithmes sur toute la collection seront bien plus naturels grâce aux ranges. En effet, les collections se comportant elles-mêmes comme des ranges, on pourra écrire :
std::vector<std::string> names { "Mehdi", "Alice", "Bob", "Eve", "Oscar", "Alexander", "John" };
std::unordered_map nicknames
{
{ "Alexander", "Sacha" }
, { "Mehdi", "Mehdidou" }
, { "Bob", "Bobby" }
, { "Johnny" }
};
auto has_nickname = [&nicknames](std::string const& name){ return nicknames.find(name) != std::end(nicknames); };
bool all_have_nicknames = std::ranges::all_of(names, has_nickname);
bool one_has_nickname = std::ranges::any_of(names, has_nickname);
Plutôt que :
std::vector<std::string> names { "Mehdi", "Alice", "Bob", "Eve", "Oscar", "Alexander", "John" };
std::unordered_map nicknames
{
{ "Alexander", "Sacha" }
, { "Mehdi", "Mehdidou" }
, { "Bob", "Bobby" }
, { "Johnny" }
};
auto has_nickname = [&nicknames](std::string const& name){ return nicknames.find(name) != std::end(nicknames); };
bool all_have_nicknames = std::all_of(std::begin(names), std::end(names), has_nickname);
bool one_has_nickname = std::any_of(std::begin(names), std::end(names), has_nickname);
En termes d’expressivité, on est à un tout autre niveau. Avec les version « Range » des algorithmes, le code est beaucoup moins verbeux, et il se lit presque comme du langage naturel !
Le concept de Vue
Les Ranges viennent avec un nouveau concept très intéressant, le concept de Vue, ou View en anglais. Les Vues sont des objets qui se comportent aussi comme des Ranges, mais elles sont copiables/assignables/déplaçables en temps constant. Grosso modo, c’est l’équivalent « Ranges » d’une paire d’itérateurs begin/end, finalement. Encore une fois, on gagne en abstraction, pour la même raison que la version « Range » des algorithmes.
On peut faire de très jolies choses avec les vues, comme itérer en partant de la fin d’un « Range », ou appliquer une transformation aux éléments pendant la traversée du conteneur, par exemple. Et ce, sans jamais utiliser des itérateurs.
La vraie puissance des Vues : les Range adaptors
On peut combiner un Range (conteneur ou Vue par exemple) avec une Vue grâce aux Range adaptors, et c’est là que l’on peut exploiter la toute puissance des Vues. On va directement le montrer sur un exemple.
Partons de l’exemple des surnoms du code précédent, et supposons que l’on veuille afficher les surnoms de tous les noms de la liste qui possèdent un surnom.
Sans utiliser d’algorithmes, on peut faire ça :
for(auto const& name : names)
{
auto nickname = nicknames.find(name);
if(nickname != std::end(nicknames)
std::cout << nickname->second << ' ';
}
Avec des algorithmes mais sans Ranges, on obtient ceci :
std::vector<std::string> output {};
auto has_nickname = [&nicknames](std::string const& name){ return nicknames.find(name) != std::end(nicknames); };
std::copy_if(std::begin(names), std::end(names), std::back_inserter(output), has_nickname);
auto get_nickname = [&nicknames](std::string const& name){ return nicknames.find(name)->second; };
std::transform(std::begin(output), std::end(output), std::begin(output), get_nickname);
std::for_each(std::begin(output), std::end(output), [](std::string const& nickname){ std::cout << nickname << ' '; });
On augmente déjà le niveau d’abstraction, mais c’est tout de même très verbeux.
Et enfin, en utilisant à fond les Ranges et les Vues, on a tout simplement :
auto has_nickname = [&nicknames](std::string const& name){ return nicknames.find(name) != std::end(nicknames); };
auto get_nickname = [&nicknames](std::string const& name){ return nicknames.find(name)->second; };
auto display = [](std::string const& s){ std::cout << s << ' '; };
std::ranges::for_each(names | std::view::filter(has_nickname) | std::view::transform(get_nickname), display);
Je trouve ce code incroyablement expressif. On va droit au but, on voit clairement ce qu’on veut faire, ça se lit très naturellement. Notez aussi qu’avec les Ranges, il est beaucoup plus facile d’appliquer des algorithmes à la chaîne.
Peut-être avez-vous remarqué que les deux dernières versions sont moins performantes que la première, car on fait deux fois la recherche dans la table. C’est dommage de devoir faire un sacrifice de performance pour gagner en expressivité.
Si cela vous intéresse, il existe une alternative aux Ranges qui permet de garder un niveau d’abstraction élevé sans faire cette concession : les smart output iterators. Il existe plusieurs articles de Fluent C++ qui abordent ce sujet très intéressant : Smart Output Iterators, The TPOIASI, How Smart Output Iterators Avoid the TPOIASI.
Le mot de la fin
Je suis très heureux que les Ranges aient été ajoutés à la bibliothèque standard, c’est un plus énorme en termes d’abstraction. Pour revenir sur le sujet de la semaine dernière, il existe des Concepts standards spécialement dédiés aux Ranges ; pensez à les utiliser si vous voulez écrire du code générique avec les Ranges !
Et voilà pour cet épisode. Dîtes moi ce que vous pensez de cette fonctionnalité fort sympathique !
A dans deux semaines ! <3