C++20 – std::span

Aujourd’hui un nouvel article pour parler d’une fonctionnalité qui devrait faire partie du C++20std::span<T> proposé dans la  P0122r5.

std::span<T> représente une vue sur une suite contiguë d’éléments. Il est inspiré de gsl::span<T>, anciennement gsl::array_view<T>, de la bibliothèque Guidelines Support Library (GSL) de Microsoft (à ne pas confondre avec cette GSL).

La GSL fournit des fonctionnalités destinées à mettre en pratique les règles proposées par les C++ Core Guidelines, qui sont rédigées, entre autre, par Bjarne Stroustrup.

La P0122r5 propose donc d’introduire std::span  dans le standard C++20, qui fournit une abstraction sur une suite contiguë d’éléments. std::span<T>  est non-owning, c’est à dire qu’il ne possède pas les éléments mais est une simple vue vers cette suite d’éléments.

Il est recommandé de l’utiliser à la place d’une paire pointeur/taille et des conteneurs contiguës ( std::vector et std::array notamment, mais également QVector et tous les conteneurs tiers pouvant être représentés par un pointeur et une taille) lorsque vous pouvez vous contenter d’une vue. Il permet en effet de proposer une interface uniforme pour ce genre de vues, d’éviter la recopie par rapport au passage d’un conteneur ou encore d’abstraire l’arithmétique des pointeurs qui peut rapidement entraîner des erreurs etc…

L’extent

Ci-suit un exemple de création d’un span de int à partir d’un tableau de style C :

Dans l’exemple précédent, le span est créé à partir du tableau de deux façons, dont nous allons voir la différence. std::span possède deux paramètres templates :

T correspond bien sûr au type des éléments, Extent quant à lui vaut soit le nombre d’éléments, qui est alors encodé dans le type à la manière de std::array, soit std::dynamic_extent (la valeur par défaut) qui indique que la taille est dynamique, c’est à dire que la taille n’est connue qu’à l’exécution.  std::dynamic_extent est simplement une constante négative :  inline constexpr std::ptrdiff_t dynamic_extent = -1;.

Dans l’exemple précédent, pour la première version (1) s1 est donc de type std::span<int, std::dynamic_extent>, soit std::span<int, -1> , correspondant à une vue sur une plage de données dynamique. Dans la seconde version (2)s2 est quant à lui de type std::span<int, 5> et est donc une vue sur une plage de taille fixe.

Création d’un span

Ci-suit une liste non-exhaustive de créations de span :

J’en oublie surement, mais l’idée est qu’un span permet d’abstraire toute suite d’éléments, dynamique ou statique.

Notez que puisque span est une vue et en ce sens n’effectue pas de copie, il n’est pas possible de créer un span à partir d’un objet temporaire ou d’un litéral :

La création d’un span à partir d’un pointeur invalide ou avec une taille hors limite est un undefined behavior.

La GSL propose également la fonction gsl::as_span, permettant notamment de profiter de l’inférence de type :

La version actuellement proposée pour C++20 ne propose pas d’équivalent std::as_span  à ma connaissance.

Opérations de base

std::span  fournit les classiques fonctions membres empty et size, ainsi que la fonction size_bytes retournant le nombre de bytes correspondant aux éléments.

size_bytes() est tout bêtement équivalent à  size() * sizeof(T). Dans mon cas, les int sont codés sur 8 bytes.

L’accès aux éléments se fait avec l' operator[] :

std::span propose également l' operator() de manière à être cohérent avec une éventuelle future implémentation multidimensionnelle. L' operator() permet effectivement de fournir une interface identique à std::mdspan,avec une dimension.  std::mdspan est un span multidimensionnel proposée dans la P0009. La GSL fournit déjà gsl::multi_span pour cet usage.

std::span offre aussi des itérateurs ainsi que les fonctions classiques ( begin, end …), permettant de l’utiliser comme n’importe quel plage de données :

Enfin, std::span possède des surcharges des opérateurs de comparaison, permettant de comparer directement deux span entre eux :

L’égalité est testée avec std::equals  et la comparaison est équivalente à std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());. L’infériorité ( operator<) est équivalente à std::lexicographical_compare(l.begin(), l.end(), r.begin(), r.end());. La comparaison est donc possible entre des span de tailles différentes.

Les subviews

std::span permet d’obtenir une vue plus petite à partir de la vue initiale. std::span::subspan permet de récupérer la vue en précisant le premier et le dernier index :

De la même manière, first et last permettent de récupérer respectivement les n premiers éléments et les n derniers éléments :

Si vous utilisez un  std::span statique, ces fonctions sont alors compile-time :

L’interface de std::span propose en plus de tout cela, les fonctions libres std::as_bytes et std::as_writables_bytes :

Ces fonctions retournent un span sur la même séquence d’éléments réinterprétés comme des bytes ( std::byte). std::as_bytes retourne un span sur des éléments constants contrairement à std::as_writables_bytes.

Conclusion

std::span n’est pas pour tout de suite dans le standard, mais vous pouvez d’ores et déjà en profiter en utilisant gsl::span (qui est légèrement différente de celle proposée pour C++20, mais vous permettra d’utiliser en plus toutes les fonctionnalités de la GSL et vous intéresser plus concrètement aux C++ Core Guidelines) ou réutiliser/créer une implémentation de la proposition. A ma connaissance, aucun compilateur ne propose encore d’implémentation pour std::span mais ça ne saurait tarder ;).

Partout où vous pouvez utiliser un pointeur et une taille, utiliser std::span puisqu’il permet de simplifier le passage des arguments ( (std::span<T> s) plutôt que (T t[], std::size_t size)) et permet de fournir une abstraction profitant de l’interface d’un conteneur et évitant les erreurs dues à l’arithmétique des pointeurs.

D’un point de vue sémantique, il permet de montrer clairement que vous ne travaillez que sur une vue plutôt que sur un pointeur ou un conteneur possédant la ressource.

N’étant qu’une vue, il évite la copie, considérez donc l’utilisation de std::span<T> à la place d’un const std::vector<T>&  par exemple.

std::span n’effectue pas de bound checking, à la manière du très contesté  std::vector::at par exemple, mais permet l’ajout d’informations de debug pour les accès hors bornes de la part de l’implémentation.

L’usage de std::span permet aussi d’écrire du code générique, qui profitera d’une interface simple d’utilisation et qui sera utilisable avec une simple paire pointeur/taille, std::vector, std::array, std::string, les tableau de style C ou encore boost::container::vector, QVectorgsl::span etc… N’hésitez donc pas à en abuser.

Merci d’avoir lu cet article et à bientôt pour plus de C++ !

Références et ressources

%d bloggers like this: