Aller au contenu principal

Un bon tableau Arduino pour votre PSC

· 4 minutes de lecture
Cédric Holocher
Président 22

Un tableau contigu est souvent la structure optimale pour utiliser une séquence d'objets en mémoire, plus particulièrement dans le monde de l'embarqué où les conteneurs du C++ moderne ne sont pas utilisables.

Cependant, les tableaux hérités du C T a[] sont maléfiques. C'est une structure de bas niveau avec un énorme potentiel de mauvaise utilisation et d'erreurs, et dans pratiquement tous les cas, il existe de meilleures alternatives : plus facile à écrire, plus facile à lire, moins sujet aux erreurs et aussi rapide. Si vous avez accès à std::array ou std::vector, ne cherchez pas plus loin ! Ce n'est hélas pas le cas sur les Arduinos que nous aimons tant.

Le problème des tableaux du C : T[N]

Les deux problèmes fondamentaux avec les tableaux C sont que :

  • Un tableau C ne connaît pas sa propre taille
  • Le nom d'un tableau C dégénère en pointeur vers son premier élément au moindre prétexte Considérez quelques exemples :
void f(int a[], int s) {
for (int i = 0; i<s; ++i) a[i] = i;
}

int arr1[20];
int arr2[10];

void g() {
f(arr1,20);
f(arr2,20); // Erreur !! Le programme fera n'importe quoi (undefined behaviour)
}

L'appel suivant écrira sur la mémoire qui ne fait pas partie de arr2 ! Vous devriez préférer la version plus simple et plus propre en utilisant la bibliothèque standard vector ou array :

void f(vector<int>& v) {
for (int i = 0; i<v.size(); ++i) v[i] = i;
}

vector<int> v1(20);
vector<int> v2(10);

void g() {
f(v1);
f(v2);
}
template<size_t N> void f(array<int, N>& v) {
for (int i = 0; i<N; ++i) v[i] = i;
}

array<int, 20> v1;
array<int, 10> v2;

void g() {
f(v1);
f(v2);
}

Puisqu'un tableau C ne connaît pas sa taille, il ne peut y avoir aucune affectation entre tableaux :

void f(int a[], int b[], int size)
{
a = b;
memcpy(a,b,size); // et encore cela ne marche que pour les type triviaux...
}
void g(vector<int>& a, vector<int>& b) {
a = b;
}

// Dans celui-ci, a et b doivent être de même taille
template<size_t N> void g(array<int, N>& a, array<int, N>& b) {
a = b;
}

Il est en outre impossible de renvoyer un tableau d'une fonction...

int* tableau() {
int a[3] = {1, 2, 3}; // détruit à la fin de la fonction
return a; // renvoie un pointeur vers de la mémoire libérée
}

std::array<int, 3> tableau() {
std::array<int, 3> a = {1, 2, 3};
return a; // marche très bien
}

La classe array<T, N>

Voici donc une classe utilisable dans vos programmes Arduinos avec un usage quasi-équivalent à std::array, utilisable avec les boucles for(auto& e : my_array) :

template <typename T, uint8_t N> struct array
{
T _data[N]; // il suffisait d'encapsuler le tableau !

public:
constexpr static uint8_t size() { return N; }
T const &operator[](uint8_t idx) const { return _data[idx]; }
T &operator[](uint8_t idx) { return _data[idx]; }
T *data() { return _data; }
const T *data() const { return _data; }
T *begin() { return _data; }
const T *begin() const { return _data; }
T *end() { return _data + N; }
const T *end() const { return _data + N; }
};
attention

Cette classe utilise uint8_t pour les index. Si vous voulez utiliser un tableau de plus de 256 éléments remplacez-le par uint16_t, uint32_t voire unsigned int (dont la taille dépend de la carte pour laquelle vous compilez ! 16 bits sur Arduino Uno, 32 bits sur PC)

Le coût : du code plus gros

En pratique, les compilateurs vont instancier le modèle (template) de la fonction pour chaque taille de tableau. Si vous êtes limités en taille de programme, considérez une approche plus poussée :


template<typename T> struct span {
T* _begin;
T* _end;

public:
span(T* begin, T* end) : _begin{begin}, _end{end}{}
template<typename E> span(E& container) :
_begin{container.begin()},_end{container.end()}{}

uint8_t size() const { return _end - _begin; }
T const &operator[](uint8_t idx) const { return _begin[idx]; }
T &operator[](uint8_t idx) { return _begin[idx]; }
T *data() { return _begin; }
const T *data() const { return _begin; }
T *begin() { return _begin; }
const T *begin() const { return _begin; }
T *end() { return _end; }
const T *end() const { return _end; }
};

Utilisable ainsi sans que les fonctions soient dupliquées :

int ma_tres_grosse_fonction(span<int> const a) {
return a.size();
}

int main() {
auto arr = array<int, 4>{1,2,3,4};
return ma_tres_grosse_fonction(arr);
}