Toujours initialiser vos variables
La différence entre les situations que vous envisagez est que le cas sans initialisation entraîne un comportement indéfini , tandis que le cas où vous avez pris le temps de l'initialiser crée un bogue bien défini et déterministe . Je ne saurais dire à quel point ces deux cas sont extrêmement différents.
Prenons un exemple hypothétique qui aurait pu arriver à un employé hypothétique participant à un programme de simulations hypothétiques. Cette équipe hypothétique essayait hypothétiquement de faire une simulation déterministe pour démontrer que le produit qu'elle vendait hypothétiquement répondait à des besoins.
D'accord, je vais m'arrêter avec le mot injections. Je pense que vous comprenez l'idée ;-)
Dans cette simulation, il y avait des centaines de variables non initialisées. Un développeur a exécuté valgrind sur la simulation et a remarqué plusieurs erreurs de type "branche sur une valeur non initialisée". "Hmm, cela semble causer un non-déterminisme, ce qui rend difficile la répétition de tests lorsque nous en avons le plus besoin." Le développeur s'est adressé à la direction, mais la direction avait un calendrier très serré et elle ne pouvait pas épargner de ressources pour détecter ce problème. "Nous finissons par initialiser toutes nos variables avant de les utiliser. Nous avons de bonnes pratiques de codage."
Quelques mois avant la livraison finale, lorsque la simulation est en mode de désabonnement complet et que toute l'équipe est prête à achever toutes les tâches promises par la direction dans un budget qui, comme chaque projet jamais financé, était trop petit. Quelqu'un a remarqué qu'ils ne pouvaient pas tester une fonctionnalité essentielle car, pour une raison quelconque, la sim déterministe ne se comportait pas de manière déterministe pour déboguer.
L’ensemble de l’équipe a peut-être été arrêté et a passé la plus grande partie de sa peine, pendant deux mois, à peindre l’ensemble de la base de code de la simulation pour réparer les erreurs de valeur non initialisées au lieu de mettre en œuvre et de tester les fonctionnalités. Il va sans dire que l'employé a ignoré les "Je vous l'avais bien dit" et a directement aidé d'autres développeurs à comprendre ce que sont des valeurs non initialisées. Curieusement, les normes de codage ont été modifiées peu de temps après cet incident, encourageant les développeurs à toujours initialiser leurs variables.
Et c'est le coup d'avertissement. C'est la balle qui a frôlé le nez. Le problème est loin beaucoup plus insidieux que vous ne le pensez même.
L'utilisation d'une valeur non initialisée est un "comportement indéfini" (à l'exception de quelques cas tels que char
). Un comportement indéfini (ou UB en abrégé) est tellement insensé et complètement mauvais pour vous, que vous ne devriez jamais jamais croire qu'il est meilleur que l'alternative. Parfois, vous pouvez identifier que votre compilateur particulier définit l'UB, puis son utilisation en toute sécurité, mais sinon, un comportement indéfini correspond à "tout comportement du compilateur." Cela peut faire quelque chose que vous appelleriez «sain d’esprit», comme avoir une valeur non spécifiée. Il peut émettre des codes opération non valides, ce qui pourrait entraîner la corruption de votre programme. Cela peut déclencher un avertissement au moment de la compilation ou même être considéré par le compilateur comme une erreur.
Ou il peut ne rien faire du tout
Mon canari dans la mine de charbon pour UB est un cas d'un moteur SQL que j'ai lu. Pardonnez-moi de ne pas le lier, j'ai échoué à trouver l'article à nouveau. Il y avait un problème de dépassement de tampon dans le moteur SQL lorsque vous passiez une taille de tampon plus grande à une fonction, mais uniquement sur une version particulière de Debian. Le bogue a été consigné consciencieusement et exploré. La partie amusante était: le dépassement de tampon a été vérifié . Il y avait du code pour gérer le dépassement de tampon en place. Cela ressemblait à ceci:
// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
// If dataLength is very large, we might overflow the pointer
// arithmetic, and end up with some very small pointer number,
// causing us to fail to realize we were trying to write past the
// end. Check this before we continue
if (put + dataLength < put)
{
RaiseError("Buffer overflow risk detected");
return 0;
}
...
// typical ring-buffer pointer manipulation followed...
}
J'ai ajouté plus de commentaires dans mon rendu, mais l'idée est la même. Si vous put + dataLength
enroulez, il sera plus petit que le put
pointeur (ils avaient des vérifications de compilation pour s'assurer que unsigned int était de la taille d'un pointeur, pour les curieux). Si cela se produit, nous savons que les algorithmes de mémoire tampon en anneau standard peuvent être perturbés par ce débordement. Nous renvoyons donc 0. Ou le faisons-nous?
En fin de compte, le débordement sur les pointeurs n'est pas défini en C ++. Comme la plupart des compilateurs traitent les pointeurs comme des entiers, nous nous retrouvons avec des comportements de débordement d'entier typiques, qui se trouvent être le comportement que nous souhaitons. Cependant, il s’agit d’ un comportement indéfini, ce qui signifie que le compilateur est autorisé à faire tout ce qu’il veut.
Dans le cas de ce bogue, Debian est arrivé à choisir d'utiliser une nouvelle version de gcc qu'aucun des autres grandes saveurs de Linux a mis à jour dans leurs versions de production. Cette nouvelle version de gcc avait un optimiseur de code mort plus agressif. Le compilateur a constaté le comportement indéfini et a décidé que le résultat de la if
déclaration serait "tout ce qui rend l'optimisation du code optimale", ce qui est une traduction absolument légale de UB. En conséquence, il a supposé que, comme ptr+dataLength
il ne pouvait jamais être en-dessous ptr
sans débordement de pointeur UB, l’ if
instruction ne se déclencherait jamais et qu’elle optimisait la vérification du dépassement de la mémoire tampon.
L’utilisation de "sane" UB a en fait causé à un produit SQL majeur un exploit pour lequel il avait écrit du code à éviter!
Ne comptez jamais sur un comportement indéfini. Déjà.
bytes_read
n’est pas modifiée (donc maintenue à zéro), pourquoi s’agit-il d’un bogue? Le programme pourrait toujours continuer de manière saine tant qu'il ne s'attend pas implicitement àbytes_read!=0
après. Les désinfectants ne se plaignent donc pas. D'un autre côté, quand ilbytes_read
n'est pas initialisé au préalable, le programme ne pourra pas continuer normalement, donc ne pas initialiser introduit enbytes_read
fait un bogue qui n'y était pas auparavant.