C++ 17 amène un type std::string_view, outil d'optimisation qui impactera les interfaces de plusieurs fonctions. Puisqu'il s'agit essentiellement d'un enrichissement à la bibliothèque standard, il est possible de concocter une version « maison » de ce type en attendant que les implémentations officielles ne commencent à se répandre.
Cet article propose une ébauche d'une telle classe, à titre expérimental et pour un expliquer l'intérêt. Je vous invite toutefois à migrer vers la version standard aussi tôt que cela s'avèrera possible, car elle sera presque inévitablement meilleure (et plus complète) que la version à vocation pédagogique présentée ici.
Il est présumé au préalable que vous êtes familière ou familier avec std::basic_string<C>; dans le cas contraire, il serait sans doute utile pour vous de prendre connaissance du fonctionnement de ce type avant de lire ce qui suit.
Le standard C++ 17 est porteur de nouveaux concepts, mais aussi d'un vaste éventail d'améliorations et d'ajouts à la bibliothèque standard. Il se trouve qu'une partie de ces raffinements influencera nos pratiques de programmation en nous enseignant ce que les expertes et les experts ont réfléchi au cours des années qui ont mené à l'élaboration du standard.
Cet article discute du type string_view, qui est un exemple de Non-Owning Container, donc d'adaptateur sur des types existants qui ne cherchent pas à être propriétaires du substrat de ces types. Ces types ont une sémantique de référence (Reference Semantics), au sens où ils jouent pour l'essentiel un rôle d'alias vers les données d'un autre type, et exposent une interface commode pour le code client – le type array_ref est un autre exemple d'un tel type (notez que std::span<T> est une meilleure solution depuis C++ 20!). L'intention est de remplacer const string& par string_view dans une importante proportion de fonctions acceptant des chaînes de caractères en paramètre.
La recommandation traditionnelle lors du passage de paramètres représentant une chaîne de caractères à une fonction qui ne les modifiera pas est de passer ces paramètres const string&. L'idée va comme suit.
Passer un const char* ou un const char(&)[N] compilera car std::string offre un constructeur implicite acceptant ces types. Dans ces deux cas, une std::string temporaire sera créée pour réaliser l'appel, et détruire à la fin de la fonction |
|
Passer une std::string ou une const std::string compilera aussi. Dans ce cas, l'objet est passé par référence et aucune copie n'est requise. |
|
L'irrirtant est que bien que le code compile et fonctionne dans chaque cas, l'utilisation de const char* et de const char (&)[N] entraîne un coût de par la copie du texte et la possible allocation dynamique de mémoire pour l'entreposer. Si la fonction appelée devait modifier la chaîne reçue en paramètre, ce coût serait justifié, mais ici, avec une fonction qui s'engage à ne pas modifier son paramètre (il est const après tout), il est permis de douter de sa pertinence.
Le type string_view vise à paller cet irritant en remplaçant le type const string&, traditionnellement préféré pour passer une chaîne de caractères à une fonction, par un adaptateur implémentant une sémantique de référence. Le principe est que cet adaptateur doit modéliser une abstraction efficace, que sa copie doit être essentiellement gratuite, et qu'il doit offir la même interface que la classe string qu'il vise à remplacer de telle sorte qu'il puisse en général remplacer cette dernière partout où cela s'avère pertinent. Je vous rappelle que la classe qui suit est une version maison, conçue pour fins éducatives, et que vous devriez privilégier std::string_view dès que vous y aurez accès.
J'ai limité ,mon implémentation à des outils standards, à une exception près, soit l'en-tête relation.h qui contient des outils pour implémenter >, <= et >= sur la base de < et la négation logique, de même que pour implémenter != sur la base de == et de la négation logique. Ces implémentations reposent sur l'idiome CRTP et sont décrites sur Truc-Barton-Nackman.html |
|
Mon basic_string_view<C> définit les types internes et publics qu'il est raisonnable d'attendre d'une std::basic_string<C>. J'ai fait une contorsion en définissant basic_string_view<C>::npos « manuellement » car je souhaitais qu'il soit constexpr et basic_string<C>::npos ne l'était pas sur le compilateur avec lequel j'ai réalisé mon implémentation. |
|
Mon basic_string_view<C> définit les attributs beg_ et end_ comme étant nuls par défaut, ce qui permet d.'avoir un constructeur par défaut qui soit implicitement constexpr dans ce cas. Notez que, puisque basic_string_view<C> vise une efficacité maximale, ce type modélise le substrat qu'il adapte comme une séquence contiguë en mémoire de type C. |
|
Chaque constructeur de basic_string_view<C> peut être exécuté en temps constant. C'est volontaire. Remarquez que pour le constructeur qui accepte en paramètre un const std::basic_string<C>& nommé s, beg_ est initialisé à la valeur de s.data() pour qu'il soit clair que l'objet construit interface directement avec le substrat de la chaîne de carctères qu'il adapte. En effet, rien n'assure qu'un itérateur sur une std::basic_string<C> soit un pointeur... |
|
Les itérateurs sont triviaux à définir, beg_ et end_ étant des pointeurs sur les extrémités d'un substrat contigu en mémoire. |
|
Les services simples que sont empty(), size(), front() et back() sont calculés en temps constant. Notez que size() est calculé, pour faire en sorte que basic_string_view<C> demeure aussi petit et léger que possible. Je n'ai oas implémenté les versions non-const de front() et de back(), mais rien ne vous empêche de le faire si tel est votre souhait. Les services que vous voudrez à tout prix éviter d'implémenter pour basic_string_view<C> sont ceux qui modifient la structure du substrat (pas de push_back() ou de resize() par exemple). |
|
J'ai exprimé l'implémentation des services sur la base de std::char_traits<C>, comme le ferait sans doute std::basic_string<C>. |
|
Notez qu'en ayant fait le choix d'utiliser CRTP pour implémenter la plupart des opérateurs relationnels, il ne me reste plus qu'à spécifier == et < pour basic_string_view<C>. |
|
Il est d'usage d'exposer pour un basic_string_view<C> un sous-ensemble des fonctionnalités habituelles pour une std::basic_string<C>, par exemple une méthode substr() donnant une perspective (une autre basic_string_view<C>, bien sûr) sur les données modélisées par l'objet. La possibilité de convertir (par un geste délibéré, donc explicit) une basic_string_view<C> en std::basic_string<C> peut être utile dans les cas où le code client souhaite réaliser des manipulations sur un objet distinct. Cette opération provoque une copie et est donc plus dispendieuse que la plupart des opérations se limitant à manipuler des basic_string_view<C>. |
|
Les alias string_view et wstring_view, par symétrie avec les alias std::string et std::wstring, sont offerts pour faciliter la tâche des programmeuses et des programmeurs. |
|
Enfin, une fonction de fabrication et un opérateur de projection sur un flux complètent le portrait; il ne serait pas à propos d'exposer un opérateur pour lire d'un flux, car celui-ci devrait être capable de redimensionner l'objet, une opération contreproductive dans le contexte. |
|
Quelques liens pour enrichir le propos.
Il y a des pièges associés à l'utilisation d'un std::string_view, et ce schéma en illustre quelques-uns :https://twitter.com/walletfox/status/999952827656081408