Aujourd’hui un nouvel article pour parler d’une fonctionnalité qui devrait faire partie du C++20 : std::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 :
1 2 3 4 5 6 7 8 |
#include <span> ... // C-like array int arr[] = {0, 1, 2, 3, 4}; std::span<int> s1{ arr }; // (1) std::span<int, 5> s2{ arr }; // (2) |
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 :
1 2 3 4 |
template< class T, std::ptrdiff_t Extent = std::dynamic_extent > class span; |
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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// A partir d'une variable int n{ 0 }; std::span<int, 1> s = n; // D'un conteneur contiguë. std::array<int, 5> arr = {0, 1, 2, 3, 4}; std::span<int> s{ arr }; // D'un pointeur et d'une taille. std::span<int> s(arr.data(), 3); // D'un tableau C. int arr[] = {0, 1, 2, 3, 4}; std::span<int, 4> s{ arr }; // De deux pointeurs. std::span<int> s{arr, arr + 4}; // D'un autre span. std::span<int> s2{ s }; |
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 :
1 2 |
std::span s{ 2 }; // error : création depuis un litéral. std::span s{1, 2, 3, 4}; // error : création depuis std::intializer_list. |
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 :
1 2 3 |
std::vector v = {1., 2., 3.}; auto s = gsl::as_span(v); auto s2 = gsl::as_span(v.data() + 1, 2); |
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.
1 2 3 4 5 6 7 |
std::array<int, 5> arr{0, 1, 2, 3, 4}; std::span<int> span{ arr }; std::cout << span.size(); std::cout << std::boolalpha << span.empty(); std::cout << span.size_byte(); |
1 2 3 |
5 false 80 |
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[] :
1 2 3 |
std::span<int, 5> s{ arr }; arr[3] = 0; |
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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template<typename T> std::ostream& operator << (std::ostream& os, std::span<T> span) { for(auto&& t : span) { os << t << ' '; } return os; } std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; std::span<int> s{v.data(), v.size()}; std::sort(s.rbegin(), s.rend()); std::cout << s << '\n'; |
Enfin, std::span possède des surcharges des opérateurs de comparaison, permettant de comparer directement deux span entre eux :
1 2 3 4 5 6 |
if(s1 == s2) std::cout << "equals !"; else if(s1 < s2) std::cout << "s1 < s2"; else std::cout << "s1 > s2"; |
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 :
1 2 3 4 5 6 |
std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; std::span<int> s{ v.data(), v.size()}; auto ss1 = s.subspan(0, 5); // {1, 2, 3, 4, 5} auto ss2 = s.subspan(4, 8); // {5, 6, 7, 8} auto ss3 = s.subspan(1, 1); // {} |
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 :
1 2 |
auto ss1 = s.first(4); // {1, 2, 3, 4} auto ss2 = s.last(2); // {9, 10} |
Si vous utilisez un std::span statique, ces fonctions sont alors compile-time :
1 2 3 4 5 6 |
std::array arr{4, 3, 5, 12, 29, 42, 9, 32}; std::span<int, 8> s{ arr }; auto ss1 = s.first<4>(s); // {4, 3, 5, 12}. auto ss2 = s.last<5>(s); // {12, 29, 42, 9, 32}. auto ss3 = s.subspan<1, 3>(s) // {3, 5}. |
L’interface de std::span propose en plus de tout cela, les fonctions libres std::as_bytes et std::as_writables_bytes :
1 2 3 4 5 6 7 8 |
// de cppreference.com template< class T, std::ptrdiff_t N> std::span<const std::byte, S/* see below */> as_bytes(std::span<T, N> s) noexcept; template< class T, std::ptrdiff_t N> std::span<std::byte, S/* see below */> as_writable_bytes(std::span<T, N> s) noexcept; // If N is std::dynamic_extent, the extent of the returned span S is also std::dynamic_extent; otherwise it is std::ptrdiff_t(sizeof(T)) * N. |
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, QVector, gsl::span etc… N’hésitez donc pas à en abuser.
Merci d’avoir lu cet article et à bientôt pour plus de C++ !
You must log in to post a comment.