Partage

Vérification des paramètres d'une fonction ?

8 juin 2012 à 17:35:15

Bonjour à tous,

C'est vendredi, aussi je me permets de vous demandez votre avis sur une question portant un peu à polémique.
Au sein de la communauté C, j'ai remarqué deux profils de programmeur : les « délégants » et les « défensifs ».
Les premiers sont ceux qui laissent les vérifications des données passées en argument à l'utilisateur d'une fonction, les seconds sont ceux qui préfèrent vérifier ce qui peut l'être.

Si l'on regarde un peu les fonctions de la bibliothèque standard, on remarque clairement un penchant pour la délégation, mis à part quelques exceptions, notamment dans les fonctions de l'en-tête <math.h> (par exemple sqrt ou log10).

Afin de rendre le débat un peu plus concret, je proposes un petit exemple de fonction : strvcat. Cette fonction est chargée de concaténer une suite de chaînes de caractères dans une autre sans dépasser une taille maximale. Voici une implantation possible par délégation :

char *
strvcat(char *s, size_t max, ...)
{
	va_list ap;
	char *ret = s;
	char *end = s + max - 1;
	char const *t;

	while (*s != '\0')
		++s;
	
	va_start(ap, max);
	while ((t = va_arg(ap, char const *)) != NULL)
		while (s < end && *t != '\0')
			*s++ = *t++;
	
	va_end(ap);
	*s = '\0';
	return ret;
}


Dans cette dernière, il est possible d'éviter deux erreurs en étant défensif : vérifier que s n'est pas un pointeur nul et vérifier que max ne vaut pas zéro. Ma question est la suivante : une solution défensive vous paraît-elle pertinente dans un tel cas et, de manière plus générale, quelle méthode utilisez-vous (délégation ou défense) et pourquoi ?

Voilà, bon vendredi à tous et merci d'avance pour vos retours :)
8 juin 2012 à 18:22:35

Personnellement, j'ai tendance à jouer la carte de la délégation.

Dans des fonctions manipulant des buffers comme ton exemple de strvcat, c'est très rare que le pointeur soit NULL :
Si il est NULL, c'est sûrement un retour de malloc qui a échoué et qui n'a pas été vérifié, ce qui est pour moi une erreur de programmation.
Sinon, le buffer est situé généralement sur la pile ou dans la section .data, et il n'y a alors pas de raisons pour que celui-ci soit NULL.

Donc si dans 99.9% des cas le pointeur passé à la fonction n'a pas besoin d'être vérifié, je ne vois pas de raisons de le faire.

Après, tout dépends aussi du niveau d'optimisation dont on a besoin pour cette fonction : si celle-ci est appelée 1M de fois dans le programme ou 10 fois, ça change la donne.

Mais pour une bibliothèque comme la libc, la performance prime sur la sécurité.
Anonyme
8 juin 2012 à 18:28:17

Personnellement, je serais plus du type défensif : je code ma fonction, et en même temps je gère tous les cas d'erreurs possible. Ainsi, j'enferme la gestion des erreurs dans ma fonction, je ne la voit plus, je ne m'en occupe plus, je ne fais que l'appeler.

Par exemple, pour la SDL, plutôt que de faire à chaque fois des vérifications pour SDL_LoadBMP, je me fais une fonction, je gère l'erreur dedans, et je n'ai plus qu'à l'appeler.

SDL_Surface * Charger_BMP(char * path)
{
    SDL_Surface * surface;

    surface = SDL_LoadBMP(path);
    if(surface == NULL)
        fprintf(stderr, "SDL_LoadBMP : %s\n", SDL_GetError());

    SDL_SetColorKey(surface, SDL_SRCCOLORKEY, SDL_MapRGB(surface->format, 255, 0, 255));

    return surface;
}
8 juin 2012 à 20:02:19

Ça dépend beaucoup du contexte, dans ton exemple informaticienzero, le risque est que le fichier de l'image ne soit pas présent. C'est un élément extérieur au programme !
Il faut donc traiter l'erreur ! Pour l'exemple de Taure, je le laisserais tel quel !
Je n'utilises ni linux, ni SDL, ni GTK.
8 juin 2012 à 20:34:20

Personnellement on m'a toujours dit que l'utilisateur était stupide et qu'il fallait gérer un maximum de cas. Ça s'applique aussi bien au programmeur qui utilise votre fonction qu'à l'utilisateur qui utilise votre programme. Donc je code plutôt défensif.

Après si c'est une bonne idée... Bien évidement ça empêche un certain nombre de segfault mais l'utilisateur (le programmeur) ne s'en rendra pas forcement compte. En revanche, en délégant il y aura bien un segfault qui lui dira clairement : "La t'as fais de la merde". Et c'est sûrement pour cette raison que les fonctions standards délèguent (en plus du léger gain de performance par l'absence de vérification).

Euh sinon... On à rien vu. ^^
SDL_Surface * Charger_BMP(char * path)
{
    SDL_Surface * surface;

    surface = SDL_LoadBMP(path);
    if(surface == NULL)
        fprintf(stderr, "SDL_LoadBMP : %s\n", SDL_GetError());
        // faudrait peut être partir avant de segfault à SDL_SetColorKey.

    SDL_SetColorKey(surface, SDL_SRCCOLORKEY, SDL_MapRGB(surface->format, 255, 0, 255));

    return surface;
}
Venez vous inscrire à la beta de GoldenPanic, c'est ici !
Anonyme
8 juin 2012 à 20:38:26

Citation : Loptr

Euh sinon... On à rien vu. ^^



Effectivement, ça m'apprendra à faire trop vite. :-°
9 juin 2012 à 12:37:34

Merci pour vos réactions :)

Citation : informaticienzero


Personnellement, je serais plus du type défensif : je code ma fonction, et en même temps je gère tous les cas d'erreurs possible. Ainsi, j'enferme la gestion des erreurs dans ma fonction, je ne la voit plus, je ne m'en occupe plus, je ne fais que l'appeler.



Attention que, comme l'a souligné Lucien63, il s'agit bien ici de la vérification de la validité des arguments reçus par la fonction. Dans l'exemple que tu donnes, il s'agirait donc de vérifier si le pointeur path est nul ;)

Citation : Tosh


Dans des fonctions manipulant des buffers comme ton exemple de strvcat, c'est très rare que le pointeur soit NULL :
Si il est NULL, c'est sûrement un retour de malloc qui a échoué et qui n'a pas été vérifié, ce qui est pour moi une erreur de programmation.
Sinon, le buffer est situé généralement sur la pile ou dans la section .data, et il n'y a alors pas de raisons pour que celui-ci soit NULL.

Donc si dans 99.9% des cas le pointeur passé à la fonction n'a pas besoin d'être vérifié, je ne vois pas de raisons de le faire.



Effectivement, pour les pointeurs je suis plutôt de ton avis, cela ne semble pas se justifier. Et dans le cas que je présente, il y a peu de chance que max soit lui-même nul. Mais il y a tout de même des cas plus fallacieux où je me demande quelle méthode est la meilleure. Par exemple :

- une fonction reçoit une taille qu'elle va utiliser pour une allocation. Doit-on vérifier que cette dernière n'est pas nulle pour éviter un comportement indéterminé (malloc de zéro) ou laisse-t-on cela à charge de l'utilisateur ?

- une fonction attends une valeur dans un intervalle précis comme sqrt ou log10 et l'utilisateur en fourni une en dehors de celui-ci (par exemple -1 pour sqrt ou 0 pour log10). Doit-on vérifier que la valeur est bien dans l'intervalle ou bien laisse-t-on courir le risque pour l'utilisateur d'obtenir une valeur invalide ou une boucle infinie ?

Citation : Loptr


Après si c'est une bonne idée... Bien évidement ça empêche un certain nombre de segfault mais l'utilisateur (le programmeur) ne s'en rendra pas forcement compte. En revanche, en délégant il y aura bien un segfault qui lui dira clairement : "La t'as fais de la merde". Et c'est sûrement pour cette raison que les fonctions standards délèguent (en plus du léger gain de performance par l'absence de vérification).



Je suis de ton avis, mais le problème c'est qu'une erreur ne survient pas forcément ou n'est pas remarquée lors des tests, ce qui peut être assez ennuyeux.
9 juin 2012 à 12:45:57

Je préfère personnelement mettre les vérifications explicitement des le code ...
Ça évite de les vérifications multiples ...
Ensuite, ça donne des fonctions plus courtes et plus claires ...
Puis, ça évite de vérifier quand on en a pas besoin ...

Donc je vote pour les codes "délégants" ... quites à faire une fonction qui vérifie ou qui gère les erreurs apart ...
Étudiant - Codeur en C | Articles | Mes projets                                                                 ache.one         Copying is an act of love.    -   
9 juin 2012 à 13:01:52

Je suis plutôt "délégants", ça permet d'avoir de meilleur performance et trouver un bug c'est relativement facile (a par pour les débutants ?).
de plus ça permet d'avoir un code général (je ne parle pas des fonctions) qui ne présente pas de bug. prendre en compte les exceptions dans la fonction peut donner l'habitude à faire des codes, source de bug avec des fonctions non protégées.


et si je me souviens bien un malloc 0 ne fait rien et n'est pas forcément source de bug ^^ (je n'ai jamais essayer malgré tout).
Anonyme
9 juin 2012 à 13:18:08

Je cite la biographie de uknow :

Citation : 29 - Pourquoi malloc(0) représente un danger ?

D'après le standard C99, l'allocation d'une taille nulle de mémoire aboutit à deux résultats possibles (aussi probable l'un que l'autre) :

  • Un pointeur NULL est retourné : Ceci est le meilleur des cas, car on sait que l'allocation a échoué, et on peut agir en conséquence.
  • Un pointeur non-NULL est retourné : C'est le désastre assuré pour le programme; pourquoi ? parce que ce pointeur n'est pas valide, et si on l'utilise ce n'est pas très différent du fait d'utiliser un pointeur non-initialisé. Dans ce cas l'échec de malloc est indétectable.



Pour prévenir cela, si vous appeler malloc (ou autres fonctions d'allocation) en utilisant une variable, assurez-vous qu'elle n'est pas nulle avant de faire l'appel.



@Taurre : ah ok. Alors dans ce cas je suis obligé de vérifier à l'intérieur de la fonction que le chemin est invalide, sinon je risque un segfault.
9 juin 2012 à 13:19:43

Oui, je suis d'accord avec @che. La validité des arguments si elle est nécessaire devrait être faîte par la fonction appelante, et non appelée pour éviter les vérifications multiples.

Exemple :

void foo(char *string)
{
   /* traitement sur la chaîne */
}

void bar(char *string)
{
   /* traitement sur la chaîne */
   foo(string);
}

int main(int argc, char **argv)
{
   if(argc > 2)
      bar(argv[1]);

   return 0;
}


Dans ce code, si on suit une approche défensive par la fonction appelée, la chaîne sera contrôlée deux fois voir trois fois !

Ici, il n'y a pas une grosse profondeur d'appel, mais imaginez qu'on ai une profondeur de 10...Ça commence à faire beaucoup de comparaisons inutiles...

Pour moi, on devrait suivre cette logique :
- Les paramètres passés aux fonctions doivent TOUJOURS suivre la spécification de la dite fonction : pas de pointeur NULL pour une chaîne/buffer, les entiers doivent toujours être dans l'intervalle spécifié, etc.

- C'est à la fonction appelante de faire les vérifications SI nécessaire.

- Une fonction ne doit PAS altérer ses paramètres (exemple plus bas).

Voici une mauvaise fonction qui touche a ses paramètres :

char* strncp(char *dst, const char *src, int len)
{
   char *p_dst = dst;

   while(*src && len)
   {
      *(p_dst++) = *(src++);
      len--;       
   }
   *p_dst = '\0';
   return dst;
}


Voici une fonction qui ne touche pas à ses paramètres :

char* strncp(char *dst, const char *src, int len)
{
   char *p_dst = dst;
   const char *p_src = src;
   int i = 0;

   while(*p_src && i < len)
   {
      *(p_dst++) = *(p_src++);
      i++;       
   }
   *p_dst = '\0';
   return dst;
}


Dans des cas où une fonction n'en appelle pas d'autre, ce n'est pas très utile, mais je trouve que c'est une forme de sécurité que de laisser les paramètres dans leurs états d'origine.

Et niveau optimisation, le compilateur peut s'en charger en supprimant les variables temporaires. Ça peut également permettre d'utiliser le mot clef register. (On peut l'utiliser directement sur les paramètres, mais ça oblige à une syntaxe particulières :) )

PS : je n'ai pas vérifié les fonctions strcp, possible qu'elle n'ai pas le même comportement ou qu'elles soient bugués :) .
9 juin 2012 à 13:57:32

J'ai tendance à déléguer à l'appelant la responsabilité du bon usage des fonctions fournies (et donc à ne pas spécifier le comportement des appels avec de mauvaises valeurs). Néanmoins, Avec ce type de programmation (de la programmation par contrat), pendant le développement d'un projet, il peut assez souvent se glisser des erreurs, des appels de fonctions qui ne respectent pas les contrats, etc... Ca peut être l'enfer à débugger. Un assert sur les conditions à remplir (pointeurs non nuls, etc...) peut faire gagner pas mal de temps.

Quels sont les avantages de la méthode ?
-le code s'arrête au premier contrat non respecté -> localisation plus aisée des bugs
-on n'a pas à réflechir à comment gérer les erreurs
-quand on ne compile plus en mode debug, ces tests sont retirés par le compilo

Les inconvénients ?
-il faut écrire le contrat à chaque fonction (certains y voient un avantage, mais c'est chiant à faire)
-ralentit le code en debug, ce qui peut amener des différences de comportement avec la release

Quand ne faut il pas utiliser d'assert ?
-Quand on ne peut pas connaitre le résultat d'une opération avant l'execution (ouverture de fichiers, de sockets, accès à une ressource externe en général)
64kB de mémoire, c'est tout ce dont j'ai besoin
9 juin 2012 à 14:11:47

Je plussoie @Nathalya. Ça me chagrinait que personne ne parle des contrats/assertions, mais voilà qui est fait (et comme j'avais la flemme de le faire moi-même, c'est parfait). :p

Ça a tendance à rendre le code plus « moche » et plus long (si on écrit des contrats longs et précis) mais bon, entre ça du code plus chiant à maintenir/déboguer, je choisis le code moche any day.
9 juin 2012 à 14:17:29

Je plussoie aussi l'utilisation de assert() ! :)
Ça peut vraiment faire gagner du temps lors de des phases de debug...
9 juin 2012 à 14:20:22

Citation : Loptr

En revanche, en délégant il y aura bien un segfault qui lui dira clairement : "La t'as fais de la merde". Et c'est sûrement pour cette raison que les fonctions standards délèguent (en plus du léger gain de performance par l'absence de vérification).


En fait, les fonctions standard délèguent pour la raison suivante :
- Il est possible de sécuriser une fonction performante (en ajoutant une surchouche sécurisée).
- Il n'est pas possible de rendre plus performant une fonction sécurisée.

Proposer des briques de base qui gènent au minimum les développements ultérieurs, quel que soit leur but, est une philosophie qui sous-tend la conception de C. Et qui rend le langage si pérenne.

Cela dit, je pense qu'il faut sécuriser le code applicatif.
Le cas de la bibliothèque standard est spécial ; il vise une réutilisabilité universelle, et c'est ce but assez particulier qui justifie d'en faire un minimum.
9 juin 2012 à 14:35:31

Pour ma part, cela dépend des projets. Dans une optique de réutilisabilité, je m'occuperai certainement d'une technique défensive consistant donc à vérifier la validité des paramètres. Maintenant, si c'est pour une petite fonction statique dont je suis sûr de l'utiliser qu'à partir de fonctions saines, je ne vais pas forcément m'embêter.

Maintenant, dans certains cas, la méthode défensive peut se heurter à des pointeurs non valides, qui ont, par exemple, été libérés sans pour autant les avoir affecté à un pointeur nul. Il y a donc d'ores et déjà une confiance qui est accordée à l'utilisateur : sa gestion correcte des pointeurs ; alors, pourquoi tester vu qu'il est possible de complètement sécuriser l'interface ?

Pour moi, l'utilisateur de la bibliothèque a parfaitement le droit de passer les paramètres qu'il veut. En contrepartie, si celui-ci utilise cette bibliothèque, c'est qu'il a besoin d'un niveau d'abstraction supplémentaire. Dans ce cas d'approche plus ADT, il me semble nécessaire de se fabriquer une interface solide.

C'est clair que la bibliothèque standard de C a pour but d'être générique dans le sens où un utilisateur ayant de fortes contraintes temporelles doit pouvoir utiliser ces fonctions. On supprime donc toute condition superflue. Donc, pour utiliser une stratégie « trust » :

  • soit on a des contraintes temps-réels (alors dans ce cas il faudra prôner délégation) ;
  • soit on a la flemme.


J'ai toutefois toujours plaisir à regarder des bibliothèques bien solides, qui permettent de se reposer un peu quand on travaille dessus (et cela évite la dimension paranoïaque quand on utilise ces fonctions). Par exemple, je trouve qu'il est beaucoup plus sympathique de travailler avec des free, dont on sait qu'il ne feront rien avec un paramètre nul, qu'avec des fonctions de <string.h>, où il faut tout checker avant d'utiliser.

Quant à l'utilisation des assertions, c'est effectivement, parfois, la bonne solution, si l'on écarte les contraintes temps-réel. Au final, on en revient donc à cette notion de temps.
Staff désormais retraité.
9 juin 2012 à 14:55:16

Citation : Tosh


Dans des cas où une fonction n'en appelle pas d'autre, ce n'est pas très utile, mais je trouve que c'est une forme de sécurité que de laisser les paramètres dans leurs états d'origine.



Cela me paraît un peu excessif non ? Dans le cas où la fonction est seule à utiliser ce paramètre il me semble que le modifier directement est plus simple et plus rapide. Par contre, je n'avais pas pensé à l'utilisation de register dans ce cas là.

Citation : Nathalya


J'ai tendance à déléguer à l'appelant la responsabilité du bon usage des fonctions fournies (et donc à ne pas spécifier le comportement des appels avec de mauvaises valeurs). Néanmoins, Avec ce type de programmation (de la programmation par contrat), pendant le développement d'un projet, il peut assez souvent se glisser des erreurs, des appels de fonctions qui ne respectent pas les contrats, etc... Ca peut être l'enfer à débugger. Un assert sur les conditions à remplir (pointeurs non nuls, etc...) peut faire gagner pas mal de temps.



Je ne connaissait pas le principe de la programmation par contrat, merci de me l'avoir fait découvrir.

Citation : rz0


Ça a tendance à rendre le code plus « moche » et plus long (si on écrit des contrats longs et précis) mais bon, entre ça du code plus chiant à maintenir/déboguer, je choisis le code moche any day.



Lorsque vous parlez de « contrat », vous faites références à des commentaires qui indiquent les valeurs attendues par la fonction ?

Citation : Marc Mongenet


Cela dit, je pense qu'il faut sécuriser le code applicatif.
Le cas de la bibliothèque standard est spécial ; il vise une réutilisabilité universelle, et c'est ce but assez particulier qui justifie d'en faire un minimum.



Tu sous-entends donc de recourir à une méthode « défensive » dans le cas du développement d'une bibliothèque par exemple ?

Citation : Lucas-84


Par exemple, je trouve qu'il est beaucoup plus sympathique de travailler avec des free, dont on sait qu'il ne feront rien avec un paramètre nul, qu'avec des fonctions de <string.h>, où il faut tout checker avant d'utiliser.



Du côté de free, la vérification du pointeur me semble plus une raison pragmatique que sécuritaire. C'est une pratique courante pour les fonctions de destruction de vérifier si leur paramètre n'est pas un pointeur nul afin de facilité la gestion d'erreur et éviter des montagnes de if (ptr != NULL). Il est d'ailleurs amusant de constater que fclose en revanche n'accepte pas un pointeur nul.

Maintenant, je suis d'accord avec toi, la technique défensive facilite la vie de l'utilisateur en lui évitant certaines vérifications.
9 juin 2012 à 15:31:41

Je dirais utltra-défensif :zorro:

En espérant ne pas être trop HS (j'ai lu la prog' par contrat) :
  • Pour plus de sécurité.
  • Pour une meilleur clarté du code dans le main.
  • Pour plus de simplicité d'utilisation (dans le main).
  • Et aussi pour moins de code à taper (dans le main) :honte:

Alors, niveau performance je ne sais pas trop.
De toute façon, les tests qu'il y a faire dans ce genre de situation, il faudra bien penser à les effectuer que ce soit en amont ou un aval ?
Donc je préfère penser en amont et profiter en aval (défensif).
Site : https://gokan-ekinci.appspot.com | Miagiste en recherche d'emploi | Profil [Dév. Java SE & EE | Dév. QlikView]
9 juin 2012 à 15:39:57

@Gugelhupf: "prog' par contrat" -> Délégant ...
C'est le principe même ...
Étudiant - Codeur en C | Articles | Mes projets                                                                 ache.one         Copying is an act of love.    -   
9 juin 2012 à 15:51:46

Citation : Taurre


Cela me paraît un peu excessif non ? Dans le cas où la fonction est seule à utiliser ce paramètre il me semble que le modifier directement est plus simple et plus rapide. Par contre, je n'avais pas pensé à l'utilisation de register dans ce cas là.



Ça permet d'être assuré que les paramètres remplissent les spécifications de la fonction à n'importe quel endroit du code.


Citation : Gugelhupf

Je dirais utltra-défensif :zorro:

En espérant ne pas être trop HS (j'ai lu la prog' par contrat) :

  • Pour plus de sécurité.
  • Pour une meilleur clarté du code dans le main.
  • Pour plus de simplicité d'utilisation (dans le main).
  • Et aussi pour moins de code à taper (dans le main) :honte:


Alors, niveau performance je ne sais pas trop.
De toute façon, les tests qu'il y a faire dans ce genre de situation, il faudra bien penser à les effectuer que ce soit en amont ou un aval ?
Donc je préfère penser en amont et profiter en aval (défensif).




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 ?

Et pour les performances, cf plus haut : il y a de nombreux cas où il n'y a pas de vérifications à faire. Et il y a également le cas où tu check plusieurs fois le même paramètre...
9 juin 2012 à 16:05:49

Citation : @ache

@Gugelhupf: "prog' par contrat" -> Délégant ...
C'est le principe même ...


Ah désolé, je ne connaissais pas ces mots :euh:
Je préfère effectuer des vérifications dans la fonction, du coup je suis défensif il me semble x°D

Citation : Tosh

Mais pour des projets avec plusieurs centaines/milliers de fonctions



Je n'ai pas l'habitude de créer une centaine/un millier de fonctions dans une page, travaillant principalement en POO (Ouh qu'est ce qu'il fout dans le forum du C, sors d'ici ! :-°) je ne dépasse généralement pas la vingtaine de fonction par classe.
Si j'avais à travailler dans un projet en C (ce que je me souhaite), je prendrais de l'avance en fixant quelques règles pour l'organisation, donc qu'il y est une centaine ou milliers de fichiers bof, ça sera bien réparti et on ne sentira pas la quantité.

Après check ou pas la question dépend du développeur et/ou du type d'application (projet personnel / en équipe).
Prenons un cas concret (ultra cliché) où on ne souhaite pas qu'il y est de division par zéro, feras-tu un check du dénominateur dans ta fonction ou laisseras-tu l'utilisateur prendre le risque de mettre un zéro ?
Site : https://gokan-ekinci.appspot.com | Miagiste en recherche d'emploi | Profil [Dév. Java SE & EE | Dév. QlikView]
9 juin 2012 à 16:12:14

Citation : Gugelhupf

Je préfère effectuer des vérifications dans la fonction, du coup je suis défensif il me semble x°D

Oui ... tu n'as donc pas compris le principe de la programmation par contrat ^^"

Après, il est vrai qu'il est préférable de trop vérifier que pas assez mais quand même ... Si on se fixe des règles strictes, le problème ne devrait pas arriver ...
La loi de Murphy nous ammène à relativiser les effets du modèle défensif :p
Étudiant - Codeur en C | Articles | Mes projets                                                                 ache.one         Copying is an act of love.    -   
9 juin 2012 à 16:57:46

@Gugelhupf : je ne t'ai pas parlé par page, mais pour un projet entier.

Et la question n'est pas de savoir si il faut vérifier les entrées utilisateur, bien évidemment qu'il faut les vérifier ! La question, c'est plutôt quand et où ? (Même si la question original traite des paramètres des fonctions, et non pas de l'environnement externe au programme)
9 juin 2012 à 17:15:33

Citation : Taurre


Citation : Nathalya


J'ai tendance à déléguer à l'appelant la responsabilité du bon usage des fonctions fournies (et donc à ne pas spécifier le comportement des appels avec de mauvaises valeurs). Néanmoins, Avec ce type de programmation (de la programmation par contrat), pendant le développement d'un projet, il peut assez souvent se glisser des erreurs, des appels de fonctions qui ne respectent pas les contrats, etc... Ca peut être l'enfer à débugger. Un assert sur les conditions à remplir (pointeurs non nuls, etc...) peut faire gagner pas mal de temps.



Je ne connaissait pas le principe de la programmation par contrat, merci de me l'avoir fait découvrir.

Citation : rz0


Ça a tendance à rendre le code plus « moche » et plus long (si on écrit des contrats longs et précis) mais bon, entre ça du code plus chiant à maintenir/déboguer, je choisis le code moche any day.



Lorsque vous parlez de « contrat », vous faites références à des commentaires qui indiquent les valeurs attendues par la fonction ?



Bah, ya la doc, c'est une chose, et il en faut ; après, si tu as des assertions ou des « contrats » dans certains langages, c'est mieux, parce que c'est de la doc exécutable.

Citation


Citation : Lucas-84


Par exemple, je trouve qu'il est beaucoup plus sympathique de travailler avec des free, dont on sait qu'il ne feront rien avec un paramètre nul, qu'avec des fonctions de <string.h>, où il faut tout checker avant d'utiliser.



Du côté de free, la vérification du pointeur me semble plus une raison pragmatique que sécuritaire. C'est une pratique courante pour les fonctions de destruction de vérifier si leur paramètre n'est pas un pointeur nul afin de facilité la gestion d'erreur et éviter des montagnes de if (ptr != NULL). Il est d'ailleurs amusant de constater que fclose en revanche n'accepte pas un pointeur nul.

Maintenant, je suis d'accord avec toi, la technique défensive facilite la vie de l'utilisateur en lui évitant certaines vérifications.



Moi chui pas d'accord. Programmer défensif, c'est juste déléguer des erreurs en plus à l'appelant, ça n'a résolu aucun problème. Programmer défensif, c'est quoi finalement ? Si on a une fonction f non-défensive qui accepte des arguments sur un ensemble A et produit un résultat sur un ensemble R (f : A -> R), programmer défensivement, ça signifie agrandir l'ensemble des arguments acceptés en rajoutant des résultats spécifiques pour ces nouvelles entrées (f : union(E, A) -> union(F, R), où E est l'ensemble des valeurs erronées prises en charge, et F l'ensemble des retours correspondant). Il faut bien comprendre qu'une fois que l'on a fait ça, l'appelant a maintenant les valeurs de F à gérer en plus, et comme souvent c'est des cas qui n'ont pas de sens, car issus d'un bug de programmation (genre concaténer NULL avec "lol"), l'appelant ne pourra rien en faire de très intelligent. Le C n'étant pas un langage sûr, quand ce genre de choses se produit, ya de grandes chances que quelque part d'autre, la maison soit en feu, de toute manière, donc ya pas grand chose que le programmeur puisse faire à ce stade...

Je pense que tout ça nous vient des gens qui veulent utiliser le C comme un langage sûr, genre Java. mais ça ne va pas fonctionner. Dans un langage comme Java, même si une erreur inattendue se produit, on aura une RuntimeException, et si on le veut vraiment, on peut essayer de revenir à un état stable dans une boucle extérieure du programme. Ya (normalement) pas de corruption mémoire, et le GC s'occupe de libérer les états qui pourraient être devenus orphelins parce que vous avez tout quitté comme un bourrin.

Parce que le C n'offre aucunes de ces garanties, à moins d'être dans un environnement spécialisé et d'utiliser des outils/conventions/méthodes spécialisés, pour avoir quelque chose de fiable, dans du code général destiné à être appelé par un programmeur lambda, dans une application lambda, je pense que ça ne sert à rien de programmer « défensivement » parce que l'appelant n'aura aucune idée de quoi faire de ces cas d'erreurs, pas plus que l'appelé. Autant rester sur un contrat plus simple (f : A -> R), travailler sur des domaines restreints d'arguments et de retours simplifie le raisonnement et les assertions ou la programmation par contrats aide à trouver les erreurs pendant le développement.

Et une dernière remarque concernant le problème de performance d'avoir des assertions dans son code : même si ça peut effectivement modifier dans le temps le comportement du programme, ça ne devrait pas avoir une influence excessive (sinon faut se poser des questions). Ça dépend ce que l'on cherche ; on peut écrire des contrats qui préservent la complexité temporelle (éventuellement de manière amortie avec des assertions qui ne s'exécutent pas à chaque appel pour les vérifications lourdes), mais parfois ça ne suffit pas. D'autre part, pour le « temps réel », ça dépend vraiment quel « temps réel », mais il faut bien comprendre que le temps réel, c'est une question de réactivité, donc avoir du code qui s'exécute en temps borné à chaque pas (c'est-à-dire des algos incrémentaux). Ce n'est pas une question de rapidité d'exécution absolue. Très souvent, les algos incrémentaux sont d'ailleurs plus lents, en terme de rendement, que leurs équivalents non incrémentaux avec des pas de taille variable. Dans ce cadre, c'est pas forcé que les contrats soient à bannir, ça dépend des contraintes...
9 juin 2012 à 18:04:51

Citation

Moi chui pas d'accord. Programmer défensif, c'est juste déléguer des erreurs en plus à l'appelant, ça n'a résolu aucun problème. Programmer défensif, c'est quoi finalement ? Si on a une fonction f non-défensive qui accepte des arguments sur un ensemble A et produit un résultat sur un ensemble R (f : A -> R), programmer défensivement, ça signifie agrandir l'ensemble des arguments acceptés en rajoutant des résultats spécifiques pour ces nouvelles entrées (f : union(E, A) -> union(F, R), où E est l'ensemble des valeurs erronées prises en charge, et F l'ensemble des retours correspondant). Il faut bien comprendre qu'une fois que l'on a fait ça, l'appelant a maintenant les valeurs de F à gérer en plus, et comme souvent c'est des cas qui n'ont pas de sens, car issus d'un bug de programmation (genre concaténer NULL avec "lol"), l'appelant ne pourra rien en faire de très intelligent. Le C n'étant pas un langage sûr, quand ce genre de choses se produit, ya de grandes chances que quelque part d'autre, la maison soit en feu, de toute manière, donc ya pas grand chose que le programmeur puisse faire à ce stade...



Justement, je pense qu'il peut être intéressant, dans le cadre d'une réutilisabilité, d'agrandir le champ de définition de la fonction, quitte à y revenir plus tard pour traiter plus intelligemment l'entrée. Par exemple, on peut décider que, pour un code nul, on fait une action particulière utile (restant dans la logique de l'utilisation de la fonction) mais qui n'encourt pas un bug. Après, c'est encore une fois à l'utilisateur de la bibliothèque de décider comment traiter le retour de la fonction. Après tout, on peut simplement retourner un unique code d'erreur, par exemple détaillé par un code d'erreur dont le fonctionnement pourrait s'apparenter à errno, et c'est ensuite à l'utilisateur d'interpréter cette marge. Alors que se retrouver avec une erreur d'exécution étrange, c'est plus dérangeant. Par exemple, si on a un ensemble d'éléments en interaction dans la fonction appelée, qui cause une erreur assez tard, ça peut être assez obscur. Par exemple (on est d'accord, c'est un exemple inutile et erroné, mais c'est pour l'exemple) :

static void
f ( void *p ) {
	unsigned long k = ( unsigned long ) p;

        /*
         * Et soudainement, bien plus loin...
         */
	printf ( "%lu\n", 42/k );
}


Supposant que je ne contrôle pas vraiment les tests sur l'appelant et si j'envoie une valeur nulle, je me retrouve avec un SIGFPE que je trouve bizarre, et à la limite je ne me rends même pas compte que le problème pourrait venir d'une valeur nulle (alors, qu'avec l'habitude, les SEGFAULT venant d'une fonction de <string.h>, on peut soupçonner un pointeur nul quelque part).

Bref, passés les assertions en phase de développement, pour remédier à ça en phase de distribution, il est nécessaire de passer par un contrôle des arguments : dans l'appelant ou dans l'appelé, telle est donc la question. Mais si jamais la fonction appelante est compliquée et qu'on ne connait pas la définition de la fonction (en gros, les opérations dangereuses qu'elle peut réaliser), difficile parfois de juger de ce qu'il faut exactement tester (avec une documentation confuse). Or, si on encapsule le tout dans la fonction appelée, on n'a plus ce problème : on sait comment ce qui est nécessaire est utilisé, et on peut traiter le domaine de définition de la fonction pertinemment.
Staff désormais retraité.
9 juin 2012 à 18:49:02

Citation : Lucas-84

Citation

Moi chui pas d'accord. Programmer défensif, c'est juste déléguer des erreurs en plus à l'appelant, ça n'a résolu aucun problème. Programmer défensif, c'est quoi finalement ? Si on a une fonction f non-défensive qui accepte des arguments sur un ensemble A et produit un résultat sur un ensemble R (f : A -> R), programmer défensivement, ça signifie agrandir l'ensemble des arguments acceptés en rajoutant des résultats spécifiques pour ces nouvelles entrées (f : union(E, A) -> union(F, R), où E est l'ensemble des valeurs erronées prises en charge, et F l'ensemble des retours correspondant). Il faut bien comprendre qu'une fois que l'on a fait ça, l'appelant a maintenant les valeurs de F à gérer en plus, et comme souvent c'est des cas qui n'ont pas de sens, car issus d'un bug de programmation (genre concaténer NULL avec "lol"), l'appelant ne pourra rien en faire de très intelligent. Le C n'étant pas un langage sûr, quand ce genre de choses se produit, ya de grandes chances que quelque part d'autre, la maison soit en feu, de toute manière, donc ya pas grand chose que le programmeur puisse faire à ce stade...



Justement, je pense qu'il peut être intéressant, dans le cadre d'une réutilisabilité, d'agrandir le champ de définition de la fonction, quitte à y revenir plus tard pour traiter plus intelligemment l'entrée. Par exemple, on peut décider que, pour un code nul, on fait une action particulière utile (restant dans la logique de l'utilisation de la fonction) mais qui n'encourt pas un bug. Après, c'est encore une fois à l'utilisateur de la bibliothèque de décider comment traiter le retour de la fonction. Après tout, on peut simplement retourner un unique code d'erreur, par exemple détaillé par un code d'erreur dont le fonctionnement pourrait s'apparenter à errno, et c'est ensuite à l'utilisateur d'interpréter cette marge.



Mouais, question d'opinion, après ; je suis d'avis que dans du code général, 9 fois sur 10, l'utilisateur ne pourra rien en faire, donc va simplement exit avec un message d'erreur au mieux.

Citation

Bref, passés les assertions en phase de développement, pour remédier à ça en phase de distribution, il est nécessaire de passer par un contrôle des arguments : dans l'appelant ou dans l'appelé, telle est donc la question. Mais si jamais la fonction appelante est compliquée et qu'on ne connait pas la définition de la fonction (en gros, les opérations dangereuses qu'elle peut réaliser), difficile parfois de juger de ce qu'il faut exactement tester (avec une documentation confuse). Or, si on encapsule le tout dans la fonction appelée, on n'a plus ce problème : on sait comment ce qui est nécessaire est utilisé, et on peut traiter le domaine de définition de la fonction pertinemment.



Si tu penses que tu as besoin de cette information au lieu du crash pour déboguer une fois en prod, à mon avis, faut pas virer les assertions en premier lieu. Après, je pense que faut pas se mentir « l'appelant contrôle les arguments », ça ne se produira pas ; l'appelant pense que ses arguments sont bons. Soit tu testes ça dans ta fonction (assertion ou programmation défensive), soit yaura aucun test.

La question, c'est principalement : est-ce que l'on crash le programme (assertion) ou est-ce que l'on laisse à l'appelant une chance de rattraper l'erreur ? Au bout du compte, c'est une question de point de vue, donc chacun fait comme il veut. Moi, je reste de l'avis que le comportement utile dominant est de terminer le programme, et dans les rares cas où l'appelant pourrait vouloir avoir son mot à dire, il peut faire les vérifications lui-même avant l'appel et décider en fonction. Je pense que ça rend l'utilisation plus simple dans le cas général où l'appelant n'a rien d'utile à faire de mieux que crasher en cas d'erreur. Mais c'est juste mon avis.
9 juin 2012 à 20:17:22

Citation : rz0

La question, c'est principalement : est-ce que l'on crash le programme (assertion) ou est-ce que l'on laisse à l'appelant une chance de rattraper l'erreur ?

En fait je vois pas en quoi c'est une chance..
Car "l'appelant" n'aura pas la chance de voir que son programme est buggue.

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.
10 juin 2012 à 13:04:06

Citation : Tosh

bien évidemment qu'il faut les vérifier ! La question, c'est plutôt quand et où ?


Le soucis c'est que si tu effectues des vérification en aval dans le main, tu vas vérifier plusieurs conditions à chaque fois que tu utilises ta fonction.
Si tu vérifies une fois dans ta fonction (même si c'est moins pratique), tu ne le feras qu'une seule fois et il n'y aura pas des risques d'oubli par exemple.

Après je me recite :

Citation : Gugelhupf

  • Pour plus de sécurité/précaution.
  • Pour une meilleur clarté du code dans le main.
  • Pour plus de simplicité d'utilisation (dans le main).
  • Et aussi pour moins de code à taper (dans le main)


Je ne sais pas si cette habitude me viens du fait que je programme en OO.
Maintenant le souci avec le C c'est qu'il n'y a pas une gestion des exceptions (try/catch, qui existe en C++) ce qui aurait nettement facilité le debug.
D'ailleurs pourquoi le goto existe et pas le try/catch ? C'est juste une version améliorée du goto pourtant (certes avec la présence d'un objet Exception, et vérification du type de l'objet dans le catch :-° )...
Site : https://gokan-ekinci.appspot.com | Miagiste en recherche d'emploi | Profil [Dév. Java SE & EE | Dév. QlikView]
10 juin 2012 à 13:41:36

Citation : Gugelhupf

Citation : Tosh

bien évidemment qu'il faut les vérifier ! La question, c'est plutôt quand et où ?


Le soucis c'est que si tu effectues des vérification en aval dans le main, tu vas vérifier plusieurs conditions à chaque fois que tu utilises ta fonction.
Si tu vérifies une fois dans ta fonction (même si c'est moins pratique), tu ne le feras qu'une seule fois et il n'y aura pas des risques d'oubli par exemple.



On ne parle pas de la même chose : tu parles des conditions dépendantes de l'environnement, qui doivent être vérifiées ; alors que le sujet de départ concerne sur les bugs de programmation, qui ne sont en général jamais vérifié explicitement. Passer un NULL à strcpy(), c'est une erreur de logique ; personne ne fait de test avant d'appeler strcpy() pour savoir « est-ce que j'ai fait une erreur de raisonnement ici », a priori.

Citation

Je ne sais pas si cette habitude me viens du fait que je programme en OO.
Maintenant le souci avec le C c'est qu'il n'y a pas une gestion des exceptions (try/catch, qui existe en C++) ce qui aurait nettement facilité le debug.
D'ailleurs pourquoi le goto existe et pas le try/catch ? C'est juste une version améliorée du goto pourtant (certes avec la présence d'un objet Exception, et vérification du type de l'objet dans le catch :-° )...



Il y a plein de gens qui codent en C++ sans exceptions, parce que ça apporte aussi son lot de problèmes. Après, goto et exceptions ont l'air vaguement similaires, mais les implémentations sont très différentes ; donc ce n'est pas juste un goto amélioré.
11 juin 2012 à 14:05:19

Citation : rz0


Bah, ya la doc, c'est une chose, et il en faut ; après, si tu as des assertions ou des « contrats » dans certains langages, c'est mieux, parce que c'est de la doc exécutable.



Citation : rz0


Le C n'étant pas un langage sûr, quand ce genre de choses se produit, ya de grandes chances que quelque part d'autre, la maison soit en feu, de toute manière, donc ya pas grand chose que le programmeur puisse faire à ce stade...



Donc, si je comprends bien, tu conseils de traduire les contrats par des assertions, de sorte que le non respect d'un contrat provoque l'arrêt du programme. Un peu comme ceci ?

assert(x >= 0);
y = sqrt(x);


Ce qui permet de repérer les erreurs lors de la phase de debug, mais d'alléger le code en release car les assertions disparaissent ? Si oui, il y a quand même des cas qui me dérange, comme celui-ci par exemple :

char *s = malloc(16);

strcpy(s, "bonjour !");


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à ?

Vérification des paramètres d'une fonction ?

× 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.
  • Editeur
  • Markdown