Bah la fonction qui contient le malloc sera donc de type int et renverra -1 ou 0, si tout s'est bien passe ou pas.
C'est pas sur ce genre de cas que la question se pose je crois.
Si l'on place l'assertion assert(s!=NULL), on sera prévenu en debug d'un échec de malloc, mais pas en release où le programme plantera lamentablement... Que faire dans ce cas là ?
En fait, si tu utilises un assert, c'est que tu considère que tu auras toujours la mémoire au moment où tu la demanderas. Si un jour tu ne l'as pas, tu es hors spec, donc tu te fiches du résultat. Les assert ne sont là que pour détecter des régressions en cours de développement. Dans certains cas, c'est acceptable, par exemple pour allouer quelques ko de mémoire pour pouvoir monter la partition avec l'OS dessus (si tu sais que ton système aura toujours assez de RAM pour faire cette opération). Dans d'autres, ça ne l'est pas, par exemple quand Gimp tente de charger une image en RAM.
C'est donc en fonction du projet qu'il faut déterminer ce qu'on peut admettre (et donc insérer dans le contrat), et ce qu'il faut vérifier et rattraper en cours d'execution (un message d'erreur indiquant qu'on manque de mémoire pour charger une image étant un rattrapage d'erreur à mon sens). La programmation par contrat n'est pas une réponse à tous les problèmes, c'est juste une solution pour indiquer qui doit garantir quoi. Si des assertions ne peuvent pas être garanties, il faut re-concevoir la partie qui s'appuyait sur ces assertions. Il est cependant certains qu'une application de release est bien plus ardue à débugger que la version de debug. Cette dernière est là pour ça.
La programmation par contrat n'est pas une réponse à tous les problèmes, c'est juste une solution pour indiquer qui doit garantir quoi. Si des assertions ne peuvent pas être garanties, il faut re-concevoir la partie qui s'appuyait sur ces assertions.
Aah ! Voilà qui est nettement plus clair
Donc, si je suis bien, exiger une valeur supérieure ou égale à zéro pour sqrt ou un pointeur et un maximum non nul pour ma fonction strvcat est raisonnable, car l'appelant peut garantir cela sans problème. Mais exiger qu'une allocation ou une ouverture de fichier réussisse, ce n'est pas viable, car il n'y a aucun moyen de le garantir, c'est bien cela ?
Dans l'idée, c'est ça. Il y a toujours des exceptions : j'ai des cas où je considère qu'une allocation mémoire va réussir, mais ce sont des cas spécifiques (parce que je sais à quel moment l'allocation sera faite et que je sais tout ce que le système aura fait avant de faire cette allocation). Quand on développe une appli sur PC, on ne peut pas considérer que ce sera toujours vrai, et un assert n'est pas une bonne idée.
Ok, merci beaucoup pour les explications
Il y a juste encore un petit point qui me chiffonne : d'après ce que j'en lis, c'est à la fonction appelée de vérifier que ses exigences sont bien respectées, ce qui me paraît logique et évite des redondances dans le code. Cependant, que faire dans le cas où l'on recourt à des fonctions d'autres bibliothèque ? On ne peut pas mettre en place les assertions dans la fonction appelée et on doit donc les mettre dans la fonction appelante, comme ceci par exemple :
assert(x >= 0);
y = sqrt(x);
Mais, dans ce cas, cela devient redondant avec les vérifications qu'effectue déjà la fonction appelante pour garantir les exigences de la fonction appelée... Il y a une autre solution ou c'est la méthode utilisée d'habitude ?
Typiquement si un mec se trimbale un tableau de 3 cases, alors vaut mieux qu'il segfault en faisant [3], plutôt que le programme continue tout de travers, ce n'est pas normal qu'il fasse [3].
Foutre plein de sécurité partout, c'est vraiment injecter le cancer dans l'application selon moi.
Imaginons un pont conçu pour supporter X tonnes. Et imaginons que le concepteur mette un mécanisme d'auto-destruction qui pulvérise le pont dès que la charge est de X+1 tonnes. En effet, il trouve qu'une auto-destruction qui tue tout le monde net, c'est plus propre qu'on pont qui s'affaisse on ne sait comment en faisant plein de blessés et quelques morts.
Penses-tu qu'un tel concepteur est un bon concepteur. Ou qu'il devrait être jugé et condamné comme criminel?
Ce n'est pas nécessaire de se fixer ce genre de règle strictes si tu as un main() et 20 fonctions...Mais pour des projets avec plusieurs centaines/milliers de fonctions, la position défensive peut vite devenir bordélique : est-ce que ce paramètre a déjà été checké dans une autre fonction, est-ce que je le check une nouvelle fois pour être sûr ?
La méthode défensif a un coût qui se paye en temps de développement on est bien d'accord. Mais d'autre part, lorsqu'on passe un paramètre (nul par mégarde) à une fonction d'une librairie, on est content de recevoir un message d'erreur claire au lieu d'un segfault. Dans ce dernier cas, le debug peut être plus ou moins long en fonction de l'expérience du codeur.
La théorie voudrait qu'une fonction bien documentée peut déléguer, mais la pratique voudrait que les codeurs ne sont pas tous sensibles à l'importance de lire la doc avant d'utiliser telle ou telle fonction, sauf assurance du contraire. Donc à voir au cas par cas.
Pour l'exemple d'une librairie qu'on code pour une utilisation "grand publique", la gestion d'erreur est très importante et contribuera à la popularité de la lib, et dans ce cas, et à mon avis, aucun à priori ne doit être fait à propos de l'utilisateur; en l'occurrence, vérifier les paramètres est une protection intéressante.
Dans un cadre plus restreint, au sein d'une équipe de développeurs (par exemple) dont on suppose (ou on connait) le sérieux, déléguer peut être une solution relativement moins coûteuse et suffisante.
Si ta fonction ne fait pas cracher le programme quand on va trop loin, alors le bug restera jusqu'en release.
Alors que si ca segfault en local, bah tu enleves directement le bug.
Jprefere enlever les bugs plutot que mettre des patchs, ou ecrire des lignes pour en annuler d'autres tsais.
Mais, dans ce cas, cela devient redondant avec les vérifications qu'effectue déjà la fonction appelante pour garantir les exigences de la fonction appelée... Il y a une autre solution ou c'est la méthode utilisée d'habitude ?
Je pense que les vérifications du contrat doivent être placés en bonne intelligence. En ce qui me concerne, j'en met sur les quelques fonctions que je propose à plus haut niveau et sur les appels aux lib que j'utilise. Ca permet de vérifier que lors des évolutions, les gens qui utilisent mon travaille lui donnent des données cohérentes, et que les lib que j'utilise me renvoient toujours le résultat attendu. Je ne fais pas des vérifications dans toutes mes fonctions.
Je pense que les vérifications du contrat doivent être placés en bonne intelligence. En ce qui me concerne, j'en met sur les quelques fonctions que je propose à plus haut niveau et sur les appels aux lib que j'utilise. Ca permet de vérifier que lors des évolutions, les gens qui utilisent mon travaille lui donnent des données cohérentes, et que les lib que j'utilise me renvoient toujours le résultat attendu. Je ne fais pas des vérifications dans toutes mes fonctions.
Je sais que je suis pénible, mais j'avoue être encore un peu perdu
Au final, j'ai l'impression que la programmation par contrat est ce que je qualifie de « programmation défensive », mise à part que la première utilise des assertions et que ces dernières disparaissent dès lors en release. Aussi, j'ai encore du mal à percevoir quand il est opportun de recourir à des assertions... est-ce que, par hasard, tu aurais un petit exemple de code, histoire d'avoir quelque chose de plus concret ?
Citation : uknow
La théorie voudrait qu'une fonction bien documentée peut déléguer, mais la pratique voudrait que les codeurs ne sont pas tous sensibles à l'importance de lire la doc avant d'utiliser telle ou telle fonction, sauf assurance du contraire. Donc à voir au cas par cas.
Je ne suis pas entièrement convaincu par cet argument. S'il ne lit pas la doc, il ne vérifiera alors pas non plus la valeur de retour et ne sera donc pas spécialement plus avancé. À moins que les fonctions de la bibliothèque ne prévoient en leur sein une gestion et un affichage des erreurs, mais là je trouve que c'est un peu excessif (bien qu'il me semble que c'est ce que fait GTK+).
Je ne suis pas entièrement convaincu par cet argument. S'il ne lit pas la doc, il ne vérifiera alors pas non plus la valeur de retour et ne sera donc pas spécialement plus avancé. À moins que les fonctions de la bibliothèque ne prévoient en leur sein une gestion et un affichage des erreurs, mais là je trouve que c'est un peu excessif (bien qu'il me semble que c'est ce que fait GTK+).
Prends l'exemple d'un autodidacte (ou autre) qui lit un tutoriel sur l'utilisation d'une librairie, c'est quand même probable qu'il commence à mettre en pratique ce qu'il a appris sans forcément consulter la doc de la librairie qu'il utilise en pensant que le tutoriel lui a transmis toutes les informations qu'il lui faut.
Bien sûr que ce n'est pas le cas de tout le monde heureusement .
La preuve tu l'as sur les forums (je ne prends pas ceux du SDZ comme référence), où généralement le simple fait de consulter la doc répond à la question du PO. D'où ma réflexion.
Je pense que les vérifications du contrat doivent être placés en bonne intelligence. En ce qui me concerne, j'en met sur les quelques fonctions que je propose à plus haut niveau et sur les appels aux lib que j'utilise. Ca permet de vérifier que lors des évolutions, les gens qui utilisent mon travaille lui donnent des données cohérentes, et que les lib que j'utilise me renvoient toujours le résultat attendu. Je ne fais pas des vérifications dans toutes mes fonctions.
Je sais que je suis pénible, mais j'avoue être encore un peu perdu
Au final, j'ai l'impression que la programmation par contrat est ce que je qualifie de « programmation défensive », mise à part que la première utilise des assertions et que ces dernières disparaissent dès lors en release. Aussi, j'ai encore du mal à percevoir quand il est opportun de recourir à des assertions... est-ce que, par hasard, tu aurais un petit exemple de code, histoire d'avoir quelque chose de plus concret ?
C'est proche, oui ; c'est une question de point de vue, surtout : est-ce que l'on accepte davantage de valeurs, ou est-ce que l'on rejette ces valeurs supplémentaires (juste de manière plus bruyante). D'autre part, t'es pas obligé d'enlever toutes tes assertions en production, si c'est ça qui te tracasse...
EDIT : Aussi, je pense que tu t'égares un peu quand tu proposes de tester les contrats dans l'appelant avant d'appeler genre sqrt(). L'idée de la programmation par contrats, c'est que l'appelant sait ou en tout cas prétend savoir que les conditions sont réalisées, et l'appelé suppose que c'est vrai ; les vérifications sont juste là pour aider au débogage, pas pour garantir quoi que ce soit... c'est pour ça que normalement on teste rarement avec assert() dans l'appelant/au milieu d'une fonction : parce que l'on sait (ou l'on pense savoir) que les conditions sont vraies, en fonction du contexte. Si tu ne peux pas déduire les conditions du contexte simplement, c'est pas bon signe, en général, ça veut dire que le code est tellement complexe qu'en le lisant, tu as des doutes sur la validité de tes propres hypothèses. Mais parfois c'est inévitable et là tu utilises un assert() dans l'appelant, mais c'est rare en pratique.
P.S. : Comme c'est un sujet intéressant, j'en ai profité pour écrire un billet sur mon blog, qui reprend et synthétise ma réponse précédente. Plutôt que d'écrire un long message sur le SdZ qui sera noyé dans la masse bien assez tôt... Si ça t'intéresse :
Cela dépends du niveau dans lequel tu développes. Une fonction bas niveau ne doit pas (ou peu) contenir de vérifications internes, on veut que ça soit juste rapide et efficace. Dès qu'on monte un peu plus haut, on aura tendance à faire des fonctions plus complètes qui utilisent des fonctions plus bas niveau, c'est celles-ci qui devraient faire les vérifications et agir en conséquence (assert ou exception). Les appels systèmes par exemple font souvent des vérifications sur les arguments et elles utilisent en interne des fonctions plus bas niveau.
Concernant les asserts, ils ne doivent pas être utilisés pour vérifier des retours de fonctions, ils ne sont qu'utiles que pour vérifier des erreurs de programmations. On veut juste avertir le développeur et seulement lui qu'il y a une erreur dans son programme, typiquement un state d'une variable qui n'aurait jamais du arriver ou un argument d'une fonction invalide.
Juste une petite précision concernant le malloc, sur un 64bits il me semble qu'il n'est théoriquement pas possible qu'il fail si on ne dépasse pas 3Gio sur une demande. C'est à dire que cela fonctionne :
#include <stdio.h>
#include <stdlib.h>
#define KIO 1024
#define TO_KIO(nb) (nb * KIO)
#define TO_MIO(nb) (TO_KIO(nb) * KIO)
#define TO_GIO(nb) (TO_MIO(nb) * KIO)
#define MAX_ITERATIONS 10000
#define ALLOCATION_SIZE TO_GIO(1)
long double byte_convert(long double bytes, char *symbol)
{
static char *symbols_table = "KMGT";
int i;
i = 0;
while (bytes >= KIO && symbols_table[i])
{
*symbol = symbols_table[i];
bytes /= KIO;
i++;
}
return (bytes);
}
void allocate(int iterations)
{
long double allocated;
int i;
char symbol;
allocated = 0;
for (i = 0; i < iterations; i++)
{
if (malloc(ALLOCATION_SIZE) == NULL)
{
printf("Malloc failed after %d iterations\n", i);
i = iterations;
}
else
allocated += ALLOCATION_SIZE;
}
symbol = 0;
allocated = byte_convert(allocated, &symbol);
printf("Managed to allocate %.3Lf %cbytes\n", allocated, symbol);
}
int main(int argc, char **argv)
{
int iterations;
if (argc > 1)
iterations = atoi(argv[1]);
else
iterations = MAX_ITERATIONS;
allocate(iterations);
return (EXIT_SUCCESS);
}
En pratique, chez moi il fail après 131070 itérations, soit en théorie 128 Tio alloués, à priori une limitation de ma MMU. Ca ne veut pas dire qu'il ne faut pas vérifier son bon fonctionnement bien sur.
C'est proche, oui ; c'est une question de point de vue, surtout : est-ce que l'on accepte davantage de valeurs, ou est-ce que l'on rejette ces valeurs supplémentaires (juste de manière plus bruyante).
Je pense que je commence à mieux saisir l'idée. Au final, on choisit entre :
- les cas qui sont considérés comme erroné mais qui « peuvent arriver » et qui justifie une terminaison normale du programme (avec exit et consort) ou d'une fonction. On peut penser à une entrée utilisateur invalide, des arguments invalides ou insuffisants, un manque de mémoire (si ce dernier n'est pas indispensable au fonctionnement du programme, comme le chargement d'une image par gimp souligné par Nathalya), etc;
- les cas qui sont considérés comme erronés mais qui « ne peuvent pas arriver » et qui justifie soit une terminaison anormale du programme avec diagnostique (à l'aide de la fonction abort par exemple) soit qu'on ne s'en occupe pas, car précisément cela « ne peut pas arriver ». On peut penser à un appel système qui échoue (comme fork), une division par zéro, une opération mathématique impossible (racine carrée d'un nombre négatif, logarithme de zéro, ...), un calcul dépassant la capacité de son type ou encore un manque de mémoire jugé anormal (impossible d'allouer une petite structure essentielle pour le programme).
le classement dans l'une ou l'autre catégorie dépendant du type de programme que l'on réalise et des goûts et sensibilités du programmeur. C'est bien cela ?
Citation : rz0
Aussi, je pense que tu t'égares un peu quand tu proposes de tester les contrats dans l'appelant avant d'appeler genre sqrt().
Effectivement, je me suis un peu perdu du côté des assertions... En fait, ce qui me dérange, c'est que comme on utilise la fonction sqrt, on ne peut pas profiter d'assertions que l'on aurait placées dans cette dernière afin de prévenir d'éventuelles erreurs d'inattention.
Citation : rz0
L'idée de la programmation par contrats, c'est que l'appelant sait ou en tout cas prétend savoir que les conditions sont réalisées, et l'appelé suppose que c'est vrai ; les vérifications sont juste là pour aider au débogage, pas pour garantir quoi que ce soit...
Citation : rFlex
Concernant les asserts, ils ne doivent pas être utilisés pour vérifier des retours de fonctions, ils ne sont qu'utiles que pour vérifier des erreurs de programmations. On veut juste avertir le développeur et seulement lui qu'il y a une erreur dans son programme, typiquement un state d'une variable qui n'aurait jamais du arriver ou un argument d'une fonction invalide.
On est bien d'accord, les assertions sont là pour traquer des erreurs d'inattention de la part du programmeur et ne sont normalement utiles qu'en phase de debug.
Citation : rz0
P.S. : Comme c'est un sujet intéressant, j'en ai profité pour écrire un billet sur mon blog, qui reprend et synthétise ma réponse précédente. Plutôt que d'écrire un long message sur le SdZ qui sera noyé dans la masse bien assez tôt... Si ça t'intéresse :
Merci pour l'article
Il y a juste une question qui me vient en le lisant : tu ne vérifies pas si le calcul de la fonction exchange_add dépasse la capacité d'un int. C'était pour simplifier l'exemple ou bien tu considère que cela ne « peut pas arriver » ? Sinon :
Citation : rz0
Si j’ai le temps et la motivation, je publierai peut-être un autre billet sur ce que j’ai appris (par l’expérience) sur l’écriture de contrats dans la pratique.
Personnellement un tel article m'intéresserais énormément
Citation : uknow
La preuve tu l'as sur les forums (je ne prends pas ceux du SDZ comme référence), où généralement le simple fait de consulter la doc répond à la question du PO. D'où ma réflexion.
C'est vrai que si la fonction SDL_LoadBMP écrivait un message du type :
C'est vrai que si la fonction SDL_LoadBMP écrivait un message du type :
SDL_LoadBMP : could not open file : ****.bmp
on aurait déjà pas mal de sujet en moins
Oui est pour le coup vu que cette fonction n'est pas appelée fréquemment au niveau performance ça ne changerais pas grand chose :).
Et si des tests comme celui ci sont utile en debug mod ils sont dénudé de sens en mode release.
d'ailleurs une petite question :
Est il possible de faire un code qui se compile uniquement en debug mod ou faut il faire systématiquement des changements manuellement ?
Est il possible de faire un code qui se compile uniquement en debug mod ou faut il faire systématiquement des changements manuellement ?
Oui, à l'aide de la compilation conditionnelle tout simplement. Par exemple en vérifiant que la macroconstante NDEBUG n'est pas définie (histoire de s'aligner sur le fonctionnement de assert) :
#ifndef NDEBUG
/*
* À compiler uniquement en debug
*/
#endif
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
Objectif Zéro Bug - le test logiciel professionnel | L'électronique de zéro | Tableaux & pointeurs | Pointeurs sur fonctions | Lecture/écriture binaire
Objectif Zéro Bug - le test logiciel professionnel | L'électronique de zéro | Tableaux & pointeurs | Pointeurs sur fonctions | Lecture/écriture binaire