<
Media
>
Article

Comprendre std::string_view de C++17

7 min
04
/
03
/
2021

Comme les user defined literals, je me suis rendu compte récemment que l'existence des string views n'était pas connue de tous. Ce n'est pas vraiment grave : il y a beaucoup d'ajouts depuis 10 ans dans le standard et les développeurs peuvent passer à côté de features non essentielles. Sans être une feature révolutionnaire ou essentielle du C++ moderne, les string views peuvent apporter un gain significatif. Découvrons tout ça ensemble !

const char*, std::string, et maintenant std::string_view

Imaginez que votre application récupère des flux de caractères dans le but de les manipuler pour faire des vérifications ou en extraire des données. Historiquement, vous avez deux solutions pour stocker et manipuler ces chaines de caractères.

La première est d'utiliser les vieilles API du C. Comme il est probable que le code qui récupère les caractères les stocke dans un simple tableau, il suffit de trimballer un pointeur vers le premier élément de ce tableau, de s'assurer qu'il y a bien un '\0' pour avoir une null-terminated byte string, et à nous les <span class="css-span">strlen()</span>, <span class="css-span">strcpy()</span> et leurs amies!

Bon OK, ça ne vend du rêve à personne... On préfère tous faire du C++ et utiliser la deuxième solution pour gérer des chaines de caractères : std::string . Le code est alors beaucoup plus simple à écrire, lire et donc à maintenir.

Depuis C++17, vous avez maintenant une troisième solution : <span class="css-span">std::string_view</span>. Voici la définition de cppreference :

The class template basic_string_view describes an object that can refer to a constant contiguous sequence of char-like objects with the first element of the sequence at position zero. A typical implementation holds only two members: a pointer to constant CharT and a size.

Leur nom suggère très bien ce que font les string views. Elles servent à manipuler des strings, mais elles ne possèdent pas la donnée : elles regardent celle de quelqu'un d'autre. En anglais, vous verrez souvent qu'elles sont qualifiées de non-owning non-mutating view. Voici un exemple de création d'une <span class="css-span">std::string_view</span> :

<pre><code>#include &lt;string_view>

const char* s ="hello, world";
std::string_view v{s};<code><pre>

Vous vous demandez certainement : "mais pourquoi avoir inventé cette classe ? quand et comment s'en servir ?". Ca tombe bien, c'est la suite de l'article.

Performances / coûts

Tout n'est pas noir avec les C-style strings et rose avec <span class="css-span">std::string</span>. Ces dernières apportent un confort et une sécurité indéniables au développeur, mais elles lui masquent certaines opérations qui peuvent être coûteuses. Typiquement, elles sont implémentées en allouant de la mémoire dans le heap. Elles savent combien de caractères sont actuellement stockés et combien pourraient être stockés au maximum. C'est pour ça qu'en plus des fonctions membres size() et length(), on a aussi capacity() :

Returns the number of characters that the string has currently allocated space for.

Toutefois, beaucoup d'implémentations utilisent en plus la Small String Optimization (SSO pour améliorer les performances des petites chaines, notamment pour les copies, en réservant un petit tableau interne à l'instance, évitant ainsi l'allocation dynamique.

Au final, une instance de <span class="css-span">std::string</span> est clairement plus volumineuse qu'un simple <span class="css-span">const char*</span>. Dans le cas de chaines en lecture seule, cette capacité à se redimensionner ne sert à rien mais est pourtant disponible. De plus, copier une <span class="css-span">std::string</span> déclenche une copie de tous les caractères associés, il faut donc faire très attention aux copies cachées dans un code où la performance compte.

<span class="css-span">std::string_view</span> a été créée pour ça : donner une API plus confortable (voir la section suivante) tout en gardant de bonnes performances pour manipuler des chaines non-modifiables.

On peut comparer l'empreinte mémoire des 3 solutions :

<pre><code>#include &lt;iostream>
#include &lt;string>
#include &lt;string_view>

int main() {
   std::cout << sizeof(const char*) << '\n';
   std::cout << sizeof(std::string) << '\n';
   std::cout << sizeof(std::string_view) << '\n';
}<code><pre>

Ce code affiche avec clang pour x64-86 :

<pre><code>8
32
16<code><pre>

Comme <span class="css-span">sizeof(std::size_t)</span> donne 8 avec ce compilateur, on retrouve bien l'implémentation typique évoquée par cppreference : un pointeur et une taille. Aucune allocation mémoire n'est faite à l'intérieur de cette classe, mais cela n'empêche pas que le pointeur pointe vers de la mémoire allouée dynamiquement.

Une API plus confortable que const char*

<span class="css-span">std::string_view</span> offre une API assez semblable à <span class="css-span">std::string</span>. Puisqu’il s’agit d’une vue sur des données non modifiables, les fonctions membres de <span class="css-span">std::string</span> qui modifient la chaîne telles que <span class="css-span">erase()</span>, <span class="css-span">push_back()</span> ou encore replace() ne sont pas disponibles pour <span class="css-span">std::string_view</span>.

Il est surtout possible d'y appliquer des algorithmes comme à n'importe quel itérateur.

A partir de C++20, on aura en bonus les très pratiques <span class="css-span">starts_with()</span> et <span class="css-span">ends_with()</span>.

Null terminated

Il y a une subtilité à connaitre : <span class="css-span">std::string_view</span> fonctionne avec des chaines qui ne se terminent pas forcément par un '\0'. Cela implique que le pointeur renvoyé par la fonction membre data() n'est pas forcément compatible pour une utilisation avec les API C :

Unlike <span class="css-span">std::basic_string::data()</span> and string literals, <span class="css-span">data()</span> may return a pointer to a buffer that is not null-terminated. Therefore it is typically a mistake to pass data() to a routine that takes just a const CharT* and expects a null-terminated string.

Cela se produit bien sûr quand les données d'origine ne contiennent pas ce caractère :

<pre><code>char array[] = {'h', 'e', 'l', 'l', 'o'}; // pas d'\0 ici
std::string_view view{array};<code><pre>

Cela se produit aussi quand on réduit la vue avec <span class="css-span">remove_prefix()</span> ou <span class="css-span">remove_suffix()</span>, ou quand on obtient une sous-vue avec <span class="css-span">substr()</span>.

Si on veut s'assurer que la chaine est null terminated, il faut convertir notre <span class="css-span">std::string_view</span> en <span class="css-span">std::string</span>. Cela fera une copie des caractères mais cela assurera la présence de l''\0'.

PS : de toute façon, le constructeur à partir d'un <span class="css-span">const char*</span> ne prend pas le l'\0 de fin :

Constructs a view of the null-terminated character string pointed to by s, not including the terminating null character.

Ainsi, le code suivant affiche 2 fois 4 :

<pre><code>const char* t = "1234";
std::string_view v1{t};
std::cout << v1.size() << '\n';

std::string_view v2{"1234"};
std::cout << v2.size() << '\n';<code><pre>

<span class="css-span">const std::string&</span> vs <span class="css-span">std::string_view</span>

Bon... Mais pourquoi ne pas juste utiliser des const <span class="css-span">std::string&</span> ? Après tout, c'est fait pour manipuler une chaine non modifiable, forcément terminée par un '\0', ça a une API cool, ça se balade facilement. Pourquoi faire des <span class="css-span">std::string_views</span> ?

Il y a des bonnes discussions sur ce sujet sur stackoverflow :

En résumé, la seule bonne raison d'utiliser des const <span class="css-span">std::string&</span> est d'avoir besoin de chaînes null terminated. Dans tous les autres cas, <span class="css-span">std::string_view</span> est un meilleur choix.

Une première raison est très simple mais pas forcément intuitive pour tout le monde : pour obtenir une référence sur une <span class="css-span">std::string</span>, il faut avoir... une <span class="css-span">std::string</span>. Et vous n'en avez pas toujours ! Quand vous construisez une instance de <span class="css-span">std::string</span> à partir d'un <span class="css-span">char*</span>, tous les caractères vont être copiés, et si la SSO n'est pas possible, il faudra faire une allocation dynamique. En revanche, aucune copie n'est faite à la création d'une <span class="css-span">std::string_view</span> à partir d'un <span class="css-span">char*</span>.

La seconde raison est qu'obtenir une sous-chaine avec <span class="css-span">std::string_view::substr()</span> a un coût négligeable puisqu'aucun caractère n'a besoin d'être copié : il suffit de définir le pointeur vers le début de la sous-chaine, sa longueur, et c'est tout. Ce n'est pas le cas avec <span class="css-span">std::string</span> puisque là encore, les caractères devront être copiés.

Dangling references

Puisqu'une vue ne possède pas les données, il faut faire attention à synchroniser les durées de vie des 2 objets. Si les données référencées sont détruites avant la vue, alors celle-ci pointe sur des données invalides. Dangling references et undefined behavior en vue ! OK, ce jeu de mots est nul.

Voici un exemple "grossier" :

<pre><code>#include &lt;iostream>

std::string_view get_view() {
   char data[] = "oupsi!";
   // --> ce tableau sera invalide dès qu'on sortira de cette fonction
   return data;
}

int main(){
 auto v = get_view();
 std::cout << v;
}<code><pre>

Ce code m'a affiché :

<pre><code>�@<code><pre>

Il y a des cas plus subtils, comme par exemple <span class="css-span">std::string_view v{std::string{"Hello"}};</span>. La vue est construite à partir d'une string qui n'existe plus dès la ligne suivante dans le code.

constexpr

Il est possible d'utiliser des string views dans des contextes constexpr.

Prenons par exemple cette fonction (voir la documentation de GCC sur ce qu'est __PRETTY_FUNCTION__) et ces quelques appels :

<pre><code>template &lt;typename T>
void f() {
      std::cout << __PRETTY_FUNCTION__;
}

f&lt;std::string>();
f&lt;std::string_view>();
f&lt;char*>();<code><pre>

Voici ce qui sera affiché :

<pre><code>void f() [with T = std::__cxx11::basic_string&lt;char>]
void f() [with T = std::basic_string_view&lt;char>]
void f() [with T = char*]<code><pre>

Imaginons maintenant que je souhaite vérifier à la compilation que le nom du type commence bien par "std::" (me demandez pas pourquoi je voudrais faire ça, c'est juste pour l'exemple). Et bien, c'est tout à fait possible :

<pre><code>template &lt;typename T>
void f() {
   constexpr auto prefix = std::string_view{"void f() [with T = "}.length();
       constexpr auto suffix = std::string_view{"]"}.length();
   constexpr auto typeLength = std::string_view{__PRETTY_FUNCTION__}.length() - prefix - suffix;
   constexpr auto name = std::string_view{__PRETTY_FUNCTION__ + prefix, typeLength};

   static_assert(name.substr(0, 5) == "std::", "Type name doesn't start with 'std::'");

   std::cout << __PRETTY_FUNCTION__;
}<code><pre>

En compilant les 3 appels montrés ci-dessus, j'obtiens une erreur de compilation :

<pre><code>prog.cc: In instantiation of 'void f() [with T = char*]':
prog.cc:22:14:   required from here
prog.cc:16:37: error: static assertion failed: Type name doesn't start with 'std::'
    static_assert(name.substr(0, 5) == "std::", "Type name doesn't start with 'std::'");
                  ~~~~~~~~~~~~~~~~~~^~~~~~~~~~<code><pre>

Conclusion

Si votre application manipule exclusivement des <span class="css-span">std::strings</span> et que vous n'avez pas besoin d'extraire des sous-chaines, vous n'aurez sans doute pas intérêt à modifier votre code pour utiliser <span class="css-span">std::string_view</span>. Mais si elle gère également des <span class="css-span">const char*</span>, cette nouveauté de C++17 pourrait vous permettre de gagner en performance et/ou en confort d'utilisation.

Pour un nouveau développement en revanche, vous aurez sans doute intérêt à utiliser des <span class="css-span">std::string_view</span> dans de nombreux cas. Gardez en tête que la gestion de la durée de vie de la donnée n'est pas aussi simple qu'avec <span class="css-span">std::string</span>.

Cette classe apporte un bon compromis entre <span class="css-span">const char*</span> et <span class="css-span">std::string</span> tout en permettant des choses supplémentaires, comme travailler sur des chaines qui ne sont pas null terminated ou dans des contextes <span class="css-span">constexpr</span>.

Si vous avez aimé cette notion de vue, notamment pour travailler avec des plain arrays, vous verrez que C++20 vous réserve quelque chose de bien sympa : std::span.

No items found.
ça t’a plu ?
Partage ce contenu
Pierre

Que la vie de Pierre, expert embarqué Younup, serait terne sans les variadic templates et les fold expressions de C++17. Heureusement pour lui, Python a tué l'éternel débat sur l’emplacement de l’accolade : "alors, à la fin de la ligne courante ou au début de la ligne suivante ?"

Homme de terrain, il est aussi à l’aise au guidon de son VTT à sillonner les chemins de forêt, dans une salle de concert de black metal ou les mains dans les soudures de sa carte électronique quand il doit déboguer du code (bon ça, il aime moins quand même !)

Son vœu pieux ? Il hésite encore... Faire disparaitre le C embarqué au profit du C++ embarqué ? Ou stopper la génération sans fin d'entropie de son bureau.