Depuis C++ 20, C++ offre un type std::span<T> pour représenter une plage dans une séquence contiguë en mémoire de T. Un std::span<T> ne copie pas ses éléments; son rôle est de représenter une plage de valeurs, alors conserver les extrémités (en pratique : un pointeur sur le premier élément et un nombre d'éléments si le nombre d'éléments est dynamique, ou plus simple encore si le nombre d'éléments est connu à la compilation).
Prenons un exemple concret.
La fonction taille() proposée à droite accepte en paramètre un span<int> et en retourne la taille, exprimée en termes de nombre d'éléments. Ce span<int> se comportera comme un tableau de int, indicé à partir de zéro, mais peut représenter une plage quelconque dans un substrat contigu en mémoire (p. ex. : les éléments de position 3 à 27). |
|
Le programme de test crée un tableau de cinq éléments. Passant ce tableau en tant que span<int>, il en affiche la taille. Comme indiqué plus haut, ceci ne copie pas le tableau (c'est une opération très légère). Par la suite, nous partitionnons le tableau, plaçant les éléments de valeur supérieure à 6 au début et les autres à la fin. Le point de partition, itérateur sur le premier élément ne respectant pas le prédicat (le premier élément de valeur inférieure ou égale à 6) est retourné et récupéré dans la variable pos. Afficher la taille de la plage de begin(tab) à pos donne le nombre d'éléments de valeur supérieure à 6. Afficher la taille de la plage de pos à end(tab) donne le nombre d'éléments de valeur inférieure ou égale à 6. Notez que j'ai utilisé CTAD ce qui permet d'écrire span plutôt que span<int>. |
|
Ceci affichera (https://wandbox.org/permlink/gSashvHRycc10Z7o) :
5
7 11 2 3 5
2
3
Notez que si nous n'avions pas span<T>, passer le tableau au complet serait possible. Il faudrait le passer par référence, cela dit, car dû aux règles du langage C, copier un tableau provoque le Pointer Decay et la fonction appelée se retrouve alors avec un simple pointeur :
#include <iostream>
using namespace std;
constexpr auto incorrect(const int tab[5]) {
return sizeof tab / sizeof tab[0]; // oups!
}
constexpr auto correct(const int (&tab)[5]) {
return sizeof tab / sizeof tab[0]; // Ok
}
template <class T, int N>
constexpr auto mieux(const T (&tab)[N]) {
return sizeof tab / sizeof tab[0]; // Ok; return N serait encore plus chic :)
}
int main() {
int tab[]{ 2,3,5,7,11 };
cout << incorrect(tab) << endl; // 2? 1?
cout << correct(tab) << endl; // 5
cout << mieux(tab) << endl; // 5
}
Si nous souhaitons passer une plage quelconque plutôt que le tableau entier, il faudra investir plus d'effort (typiquement, on opérerait alors avec une paire d'itérateurs).
Contrairement à std::string_view qui représente une vue immuable sur une string, un std::span<T> permet de modifier les éléments de la plage si le type T lui-même est mutable. Pour écrire une fonction opérant sur une plage immuable, il suffit d'exprimer cette fonction sur un std::span<const T> comme le montre l'exemple suivant :
#include <iostream>
#include <iterator>
#include <span>
using namespace std;
void afficher(span<const int> sp) {
for(auto n : sp)
cout << n << ' ';
cout << endl;
}
template <class F>
void modifier(span<int> sp, F f) {
for(auto &n : sp)
n = f(n);
}
int main() {
int tab[]{ 2,3,5,7,11 };
afficher(tab); // 2 3 5 7 11
modifier(tab, [](int n) { return n * 2; });
afficher(span{ next(begin(tab)), prev(end(tab)) }); // 6 10 14
}
Au moment d'écrire ceci, la déduction des paramètres d'un span dans du code générique semble poser quelques problèmes. Ainsi, ceci ne compilera pas :
#include <iostream>
#include <iterator>
#include <span>
using namespace std;
template <class T>
void afficher(span<const T> sp) {
for(auto n : sp)
cout << n << ' ';
cout << endl;
}
int main() {
int tab[]{ 2,3,5,7,11 };
afficher(tab); // zut
}
... car la génération de code verra tab comme un int* et ne trouvera pas de constructeur approprié, et ceci ne compilera pas :
#include <iostream>
#include <iterator>
#include <span>
using namespace std;
template <class T>
void afficher(span<const T> sp) {
for(auto n : sp)
cout << n << ' ';
cout << endl;
}
int main() {
int tab[]{ 2,3,5,7,11 };
afficher(span{ tab }); // zut
}
... car la génération de code se plaindra de non-concordance sur le plan des qualifications const en générant le code (de span<T> à span<const T>), mais ceci compilera sans peine :
#include <iostream>
#include <iterator>
#include <span>
using namespace std;
template <class T>
void afficher(span<const T> sp) {
for(auto n : sp)
cout << n << ' ';
cout << endl;
}
int main() {
int tab[]{ 2,3,5,7,11 };
afficher(span<const int>{ tab }); // 2 3 5 7 11
}
Quelques liens pour enrichir le propos :