mercredi 21 avril 2010

C++ & STL : les pièges du conteneur 'vector'

Le conteneur 'vector' est très pratique d'utilisation mais souffre à mon avis d'un défaut majeur: il est trop tolérant avec l'utilisateur, ce qui se retourne après (souvent...) contre celui-ci.

Par exemple, l'opérateur [] est défini, ce qui rend l'usage de ce conteneur intuitif pour les programmeurs venant du C, en remplacement des tableaux. On pourra ainsi écrire:
vector<int> tab(10);
tab[0] = 3;

de la même façon qu'on écrivait en C:
int tab[10];
tab[0] = 3;

Mais ceci se révèle après coup TRES DANGEREUX. En effet il n'y a pas de vérification de la validité de l'indice dans cette notation. En vérifiant dans l'implémentation de Mingw, on y trouve le commentaire suivant:
This operator allows for easy, array-style, data access. Note that data access with this operator is unchecked and out_of_range lookups are not defined.

Ceci est d'ailleurs rappelé sur la page Wikipedia du conteneur 'vector'.

Et que ce passe-t-il en pratique ? Et bien, loi de Murphy oblige, ça arrive un jour ou l'autre (croyez moi...). Et donc, crash. Bon, un crash, soit, mais... et alors me direz vous ?
Et bien, ce type de crash est lié à une corruption mémoire, et le problème, c'est qu'il est extrêmement difficile à pister. Contre toute attente, le crash ne se produit pas lors de l'exécution de l'accès au vector, mais à un autre endroit du programme, là où rien de spécial n'est exécuté...

En conclusion, si vous avez moins de 5 ans d'expérience en C++, et que vous travaillez sur un projet conséquent en C++, ne jamais utiliser la notation '[]' mais TOUJOURS la méthode 'at()', qui effectue la vérification de validité d'indice.

* Liens:
Edit 20121102: En fait, sous GCC, on peut activer le bound checking pour les conteneurs de la STL. Il suffit de compiler en définissant le symbole _GLIBCXX_DEBUG (soit l'ajout du flag -D_GLIBCXX_DEBUG). Ceci est décrit ici. Source: stackoverflow.com/questions/1290396.

4 commentaires:

  1. Effectivement.

    Plusieurs remarques toutefois :

    1- Même si la méthode at() fait la vérification, si un code produit un indice hors limites alors ce code là doit être corrigé. Ce genre de vérification est à la charge du programmeur ; c'est la logique C/C++. Sinon faut faire du Java ou du .NET.
    2- L'utilisation de [] est plus rapide que l'utilisation de at(). Donc si c'est juste pour bénéficier de "quelques" vérifications heureuses et perdre du temps, il vaut mieux revoir le code et utiliser []. Après si c'est pour faire une caisse enregistreuse, c'est sur que at() n'est pas gênant.
    3- L'opérateur [] est plus dans le respect du C/C++ (bas niveau et charge au programmeur de vérifier son code) que at() qui se positionne comme une fonction de plus haut niveau.
    4- Enfin, tu as boost pour travailler avec des objets C++ plus puissants http://www.boost.org/.

    RépondreSupprimer
  2. Quelques commentaires sur le commentaires...
    1- Même si la méthode at() fait la vérification, si un code produit un indice hors limites alors ce code là doit être corrigé
    Toutafé. Le sens de mon post est simplement d'avertir pour éviter de perdre des heures voire plus, sur ce ce qui ne devrait pas arriver...
    2- L'utilisation de [] est plus rapide que l'utilisation de at()
    Ben oui, logique, vu qu'il y a un check en moins. Mais sauf cas extrême, le gain sera minime. Et, une fois le code déboggé, il me semble plus facile (via une macro par exemple) de redéfinir la méthode at() par une version sans check que l'inverse.
    3- L'opérateur [] est plus dans le respect du C/C++
    La dessus, il y aurait à débattre, je ne vois vraiment pas pourquoi.
    4- Enfin, tu as boost
    Oui, merci ;-) J'utilise quelques composants Boost depuis quelque temps déjà...

    RépondreSupprimer
  3. 3- L'opérateur [] est plus dans le respect du C/C++
    La dessus, il y aurait à débattre, je ne vois vraiment pas pourquoi.

    Oui c'est vrai. Maintenant que je me relis, je ne sais plus à quoi je pensais quand j'ai écrit ça. Pour moi le C/C++ doit rester un langage qui permet d'être performant en rapidité ; c'est peut-être par rapport à ça. Toujours est-il que quand j'ai écrit la totalité du commentaire je pensais évidemment à du traitement d'image. Et là pour une image, il serait in-envisageable de rajouter un check par pixel. Un at() dans une fonction optimisée de traitement d'images n'a pas sa place.

    Pour revenir à ton plantage. Un accès hors limites est très vicieux. Tu peux écrire n'importe où dans la zone mémoire allouée à l'exécutable. Tu peux notamment modifier la valeur d'un pointeur, c'est à dire l'adresse pointée vers le pointeur et là, à son prochain accès c'est le crash assuré. D'où le plantage qui se produit ailleurs et pas forcément au moment de l'accès hors limite. Avec un peu de chance, tu peux juste écrire en toute légalité dans un autre de tes tableaux. Et là tu n'auras jamais de crash et tu ne t'apercevras peut-être jamais de cet accès hors limite. Très vicieux, n'est ce pas ?

    Sous Linux, avec gcc, il y a l'option de compilation "-lefence" (pour Electric Fence) qui permet d'allouer la mémoire de chaque élément créé dynamiquement en fin de page mémoire. Ce qui fait qu'un accès hors limite supérieure va provoquer un clash directement au niveau du code ayant fait cet accès. Je ne sais quel compilo tu utilises mais peut-être tu peux trouver un équivalent à lefence. Très précieux pour déboguer.

    RépondreSupprimer
  4. Je ne sais quel compilo tu utilises
    GCC mais sous XP |-(. Et gdb a été incapable de trouver la cause du crash. J'ai finalement compilé sous Linux, et là, Valgrind m'a trouvé instantanément le bug...

    RépondreSupprimer