Profil de paraze

  • Inscrit 10/07/14 à 20h28
  • Dernière visite sur le site : 21/04/15 à 22h43

Derniers sujets créés


Biographie

nyanpasu~

Sans intro, vous trouverez ici quelques infos et conseils de prog’ qui peuvent s’avérer être plutôt pratiques, en particulier lorsque l’on débute. Bien que je parle beaucoup du C, je pense néanmoins que l’on peut en tirer quelques enseignements un brin plus généraux.

TL;TR

Voici un petit résumé de ce qui pourrait être un mémento de conseils de codage divers, ainsi qu’un court résumé d’une conversation où des monstres comme Fvirtman, uknow ou encore SofEvans, débattaient sur « comment programmer plus efficacement » :

  • Indentez votre code
  • Évitez les répétitions, factorisez-le
  • Vérifiez les retours de vos fonctions à risques
  • Utilisez des options de compilation pertinentes
  • Lisez les documentations ainsi que les pages man
  • Nommez intelligemment vos variables, fonctions, et structures (les identificateurs en somme)
  • Lorsque vous commettez une étourderie, posez-vous la question « pourquoi et comment je l’ai faite ? », et aussi « comment ne plus la reproduire ? »
  • Une fonction = Une action. Les fonctions crées sont en général plutôt courtes et efficaces. KISS toussa.
  • Voici les questions à se poser :
    • Lorsque vous appellez une fonction
      • Qu’est-ce qui peut faire bugger son appel ?
      • Quel serait le résultat attendu ?
      • Et si le résultat ne correspond pas à ce que j’attendais ?
    • Lorsque je crée une fonction :
      • Comment pourrais-je signaler une erreur de traitement (pour que la fonction appelante s’en rende compte) ?
      • Qu’est-ce que j’attends comme valeur pour mes arguments ?
      • Et si je n’ai pas la valeur attendue ?
      • Comment notifier précisément l’erreur rencontrée ?
      • Que dois-je faire comme traitement de secours en cas d’un argument erroné ?
      • Est-ce que la valeur que je vais retourner sera valide en dehors du contexte actuel (vérifier la durée de vie de l’objet retourné) ?

Débusquer et apprivoiser de simples bugs — bonnes pratiques — en C

Paraît-il que c’est important.

L’indentation

Indenter d’un code signifie insérer des espaces, tabulations, retours à la ligne, dans le but bien précis de le clarifier et d’améliorer sa lisibilité. Un code bien indenté permet au programmeur de mieux cibler les erreurs de syntaxe, et progressivement, d’en faire de moins en moins. Outre cela, cette pratique a aussi l’avantage de paraître (le paraître, parlons-en) plus sérieux sur un forum. Anéfé, un code moche, non-indenté, peut anéantir votre crédibilité et vous attirez de nombreux ennuis ; ennuis que je comprends mais que je ne cautionne pas (savoir faire une recherche, indenter un code, le colorer sur un forum, n’est pas inné chez le débutant lambda, mais ce n’est pas une raison pour l’incendier, je comprends néanmoins que cela puisse être énervant à la longue).

Un exemple vaut mille mots :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAX 100
int main(void)
{int nb=0,test=0; srand(time(NULL));
nb=rand()%MAX+ 1;
do
{
printf(">>> ");
scanf("%d", &test);
if (nb>test) printf("+\n");
else if (nb < test)
printf("-\n");
}while(test!=   nb);
printf("Nice.\n");
return 0;
}

Voyez plutôt :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX 100

int main(void)
{
    int nb = 0, test = 0;

    srand(time(NULL));
    nb = rand() % MAX + 1;

    do
    {
        printf(">>> ");
        scanf("%d", &test);

        if (nb > test)
            printf("+\n");
        else if (nb < test)
            printf("-\n");
    } while (test != nb);

    printf("Nice.\n");

    return 0;
}

Vous me direz peut-être que cet exemple est un brin exagéré, mais vous êtes loin du compte : au fil des années, j’ai rencontré certains codes, sur des sites bien connus comme OpenClassrooms, qui pourraient remporter les IOCCC, s’ils avaient le mérite d’être rigolo. ^^’

Je tiens à attirer votre attention sur l’existance d’indenteurs automatiques. Certains aiment, d’autres pas. « Les goûts et les couleurs » me direz-vous. Cette phrase est toujours un brin inutile je trouve. Fondamentalement, c’est évident qu’on ne pas forcer une personne à aimer ceci ou cela, mais ça n’empêche pas les remarques, on peut aimer quelque chose tout en y portant un regard lucide ; dire que la variété française vaut du Mozart c’est de la connerie, dire qu’on préfère cela malgré un niveau musical plus bas et une approche brutale pourquoi pas. Je diverge. Je pense cependant qu’un programmeur devrait être capable d’assurer un minimum l’indentation d’un code. Certaines personnes préconisent au contraire l’utilisation d’outils spécialisés et plutôt efficaces, pour ma part, je ne les utilise que très rarement, pour indenter un code immonde trouvé sur le web en fait (:magic:), ou pour avoir un aperçu de ce qu’à quoi mon code pourrait ressembler avec un autre style d’indentation parfois.

Subtile transition s’il en est.

Je ne m’étenderai cependant pas sur le sujet, je vous invite plutôt à lire la page wikipedia associée.

Options de compilation

Et si nous étions plus rigoureux ?

Il existe plusieurs sortes d’options de compilation, certaines servent au débogage, d’autres à l’optimisation, et celles qui nous intéressent ici, les options d’avertissement. Rapidement, une option d’avertissement indique au compilateur de nous avertir si certaines lignes de code lui semblent potentiellement bug-ogène, si je puis dire, mais qui ne sont pas intrinsèquement erronées.

Prenons pour exemple le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

void foo(int **tab)
{
    tab[0] = 42;
}

int main(void)
{
    char tab[42][42] = {{0}};
    int a = 5;
    double b = 3.14;

    foo(tab);

    a /= b;

    printf("%f", a);

    return 0;
}

Sans configuration particulière, voici la sortie du compilateur GCC :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
main.c: In function 'foo':
main.c:5:12: warning: assignment makes pointer from integer without a cast
     tab[0] = 42;
            ^
main.c: In function 'main':
main.c:14:5: warning: passing argument 1 of 'foo' from incompatible pointer type
     foo(tab);
     ^
main.c:3:6: note: expected 'int **' but argument is of type 'char (*)[42]'
 void foo(int **tab)

Là, c’est tout bonnement trop grave pour laisser passer une telle bêtise. Avec de la chance, le programme généré pourrait vaguement fonctionner, mais rien est moins sûr. Nous faisons face ici à un comportement indéterminé. La sémantique de certaines opérations n’étant pas explicitement définies par les normes du langage (C89, C99, C11). leurs comportement est spécifié comme arbitraire, et donc laissé au bon vouloir du compilateur. Évitez donc les overflow d’entiers signés (lorsqu’un int dépasse les bornes, littéralement) par exemple, ou encore, cette liste exhaustive : <http://port70.net/~nsz/c/c89/c89-draft.html#A.6.2>, si vous utilisez le C89.

Il est possible de forcer GCC à y mettre plus de cœur, à être un brin moins permissif :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
main.c:3:6: warning: no previous prototype for 'foo' [-Wmissing-prototypes]
 void foo(int **tab)
      ^
main.c: In function 'foo':
main.c:5:12: warning: assignment makes pointer from integer without a cast
     tab[0] = 42;
            ^
main.c: In function 'main':
main.c:14:5: warning: passing argument 1 of 'foo' from incompatible pointer type
     foo(tab);
     ^
main.c:3:6: note: expected 'int **' but argument is of type 'char (*)[42]'
 void foo(int **tab)
      ^
main.c:16:10: warning: conversion to 'int' from 'double' may alter its value [-Wfloat-conversion]
     a /= b;
          ^
main.c:18:5: warning: format '%f' expects argument of type 'double', but argument 2 has type 'int' [-Wformat=]
     printf("%f", a);

Nous avons ainsi une idée plus précise de ce qui pourrait potentiellement faire bugger le programme, ou même le faire comporter de manière complètement dissidente et irrespectueuse (le prog’ ne plante pas, mais il se comporte de manière indésirable).

Je ne pense pas vous surprendre en affirmant qu’il existe différents compilateurs C, je pourrais vous parler de Clang, que j’affectionne tout particulièrement, notamment pour la clarté des messages d’erreurs et d’avertissements (… si ce n’était que ça). Je vous renvoie à ce bref comparatif pour plus de détails.

Si vous travaillez avec GCC :

1
2
3
gcc -Wall -Wextra main.c -o prog

`

Les options -Wall et -Wextra rassemblent à eux deux une quarantaine d’options différentes : https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#Warning-Options. Lorsque j’utilise ce compilateur, il m’est impossible de ne pas utiliser ces deux options, options que je considère comme essentielles. Vous pouvez allez plus loin en compilant ainsi :

1
gcc -Wall -Wextra -Wswitch-default -Wunreachable-code -Winit-self -Wshadow -Wwrite-strings main.c

Ou même, si vous avez des tendances paranoïaques par forcément toujours utiles :

1
gcc -Wall -Wextra -Waggregate-return -Wcast-align -Wcast-qual -Wconversion -Wformat=2 -Winline -Wlong-long -Wmissing-prototypes -Wmissing-declarations -Wnested-externs -Wno-import -Wpointer-arith -Wredundant-decls -Wreturn-type -Wshadow -Wstrict-prototypes -Wswitch -Wwrite-strings -Wsuggest-attribute=pure -Wsuggest-attribute=const -Wsuggest-attribute=noreturn -Wvla main.c

Je vous laisse vous renseigner pour Clang et les autres (tcc, lcc, pcc, vbcc, Visual C++, etc.), je peux néanmoins vous dire qu’il existe l’option magiq -fsanitize=undefined : Clang intègre un module de détection de comportements indéfinis, rien que ça. Une large partie de ce qui n’a pas été vu à la compilation sera débusqué par ce module (attention, cela reste un parasite greffé à votre programme). L’option -Weverything est plutôt sympathique aussi pour les flemmards.

Nous sortons du contexte des avertissements, mais je vous invite à vous pencher sur cette option d’optimisation fort intéressante : -O (pour le débuggage, utilisez -g ainsi que votre moteur de recherche préféré). Certains affirmeront avec dégoût presque que les compilateurs font « tout le boulot de nos jours »… Où est le soucis ? Ils sont conçus pour ça. J’ai un peu l’impression que c’est à peu de choses près le même combat basique qu’avec les langages dits de haut niveau et de bas niveau : « c’est nul on a plus rien à faire on nous mâche le travail » (cet argument ne vole pas très haut en effet), ou encore à une autre échelle : « c’est nul CryEngine c’était mieux avant avec OpenGL ».

Pour recentrer mon propos, je pense que l’évolutivité et la lisibilité du code sont largement plus importants que quelques « optimisations » manuelles, certes potentiellement intéressantes, mais que le compilo gère bien mieux que nous, pauvres humains. Bien sûr, nous allons éviter les monstruosités les plus immondes et évidentes, mais ’pas besoin de se prendre la tête avec de petites optimisations chronophages qui peuvent s’avérer très complexes et contre-productives.

Pourquoi ne pas se concentrer plutôt sur le problème en lui même, et non sur ce qui peut au mieux nous ralentir, et au pire détruire toute la beauté de son code, le ralentir, et ralentir notre productivité elle-même ?

Vérifiez les retours de vos fonctions

En C, il existe une règle intrinsèque transcendant toutes les autres : « un malloc = un free ». Je me permets d’en rajouter une autre au moins aussi importante : « une fonction à risque = une vérification », sachant qu’idéalement, nous devrions vérifier le bon fonctionnement de toutes fonctions, mais limitons-nous à cela dans un premier temps. Qu’est-ce qu’une fonction à risque ? Je n’ai pas de définition universelle à vous donner, les quelques exemples suivant se suffiront à eux-même :

  • malloc
  • fopen
  • IMG_Load
  • TTF_OpenFont
  • SDL_Init

Un petit code pour illustrer la chose.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>

int main (int argc, char *argv[])
{
    SDL_Surface *screen = NULL, *image = NULL;

    SDL_Init(SDL_INIT_VIDEO);

    screen = SDL_SetVideoMode(800, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
    image = SDL_LoadBMP("image.bmp");

    SDL_BlitSurface (image, NULL, screen, NULL);
    SDL_Flip(screen);

    SDL_Delay(3000);

    SDL_FreeSurface(image);

    SDL_Quit();

    return 0;
}

Ce code met en évidence une limite des warnings. Ce code semble en effet correct au premier abord, mais c’est une vraie bombe à retardement. Il suffit que la fonction SDL_LoadBMP aie un problème pour faire planter le programme (en lui indiquant un mauvais chemin d’accès). Bien qu’elle apparaîsse comme la fonction la plus dangereuse du code, les autres ne sont pas pour autant bénignes. Une cause courante d’erreur de la fonction SDL_Init est l’utilisation d’un affichage particulier sans avoir le support du sous-système correspondant, tel qu’un pilote de souris manquant en utilisant un périphérique de framebuffer.

Et plus généralement, lorsque l’on appelle une fonction, elle peut réussir comme elle peut échouer. Le retour de cette fonction permet justement de s’en assurer. En ignorant le retour d’une fonction, le programme peut cracher sans prévenir. En testant cette valeur, vous saurez le pourquoi du comment, et ainsi éviter un crash sordide (SEGFAULT ou autre).

Comment connaître ce que renvoie une fonction me direz-ous. Rien de plus simple vous réponderais-je, RTFM : lisez la doc’ !

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>

int main (int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *image = NULL;

    if(SDL_Init(SDL_INIT_VIDEO) == -1)
    {
        fprintf(stderr, "Erreur d'initialisation de la SDL : %s\n", SDL_GetError());
        exit(EXIT_FAILURE);
    }

    if((ecran = SDL_SetVideoMode(800, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL)
    {
        fprintf(stderr, "L'initialisation du mode video n'a pas fonctionne : %s\n", SDL_GetError())
        exit(EXIT_FAILURE);
    }

    image = SDL_LoadBMP("image.bmp");
    if(image == NULL)
    {
        fprintf(stderr, "Erreur lors du chargement de l'image : %s\n", SDL_GetError())
        exit(EXIT_FAILURE);
    }

    SDL_BlitSurface (image, NULL, ecran, NULL);
    SDL_Flip(ecran);

    SDL_Delay(3000);

    SDL_FreeSurface(image);

    SDL_Quit();

    return 0;
}

Codez malin, convevez une fonction qui charge une image de manière sécurisée (à coupler avec SDL_DisplayFormat par exemple) !

Factorisez votre code

Ou encore « codez élégament et intelligement ». Vous connaissez sans doûte le jeu Plus ou Moins. Je ne trouve point de mots pouvant être plus explicite que ce code :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
printf("Quel niveau ?");
scanf("%d", &niveau);

if (niveau == 1)
{
    nb = rand() % 10 + 1;

    do
    {
        printf(">>> ");
        scanf("%d", &test);

        if (nb > test)
            printf("+\n");
        else if (nb < test)
            printf("-\n");
    } while (test != nb);

    printf("Nice.\n");
}

if (niveau == 2)
{
    nb = rand() % 100 + 1;

    do
    {
        printf(">>> ");
        scanf("%d", &test);

        if (nb > test)
            printf("+\n");
        else if (nb < test)
            printf("-\n");
    } while (test != nb);

    printf("Nice.\n");
}

/* ... */

Une atrocité, et je pèse mes mots. Il m’est arrivé de rencontrer des plus ou moins de plus de 1200 lignes, et ce, uniquement à cause d’ignominies similaires. La boucle entière du jeu est la même pour chaque niveau, la seule instruction qui diffère, c’est celle de la génération pseudo-aléatoire. Un exemple de factorisation possible :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
printf("Quel niveau ?");
scanf("%d", &niveau);

switch(niveau)
{
    case 1:
        max = 10;
        break;
    case 2:
        max = 100;
        break;
    case 3:
        max = 1000;
        break;
    /* ... */
    default:
        max = 100;
        break;
}

On évite ainsi plusieurs dizaines, voir centaines de lignes. Code que l’on pourrait encore factoriser :

1
2
3
4
5
6
7
printf("Quel niveau ?");
scanf("%d", &niveau);

if (niveau >= 0 && niveau <= 6)
    max = pow(10.0, niveau); /* Avec des casts : max = (int)pow(10.0, (double)niveau) */
else
    max = 100;

Imaginez que vous souhaiteriez programmer un 2048, la duplication du code est facile (un grand nombre de lignes de code pour chaque direction, alors qu’il n’y a que peu de différence au final), mais à éviter absolument. Un code inutilement dupliqué ne peut que vous apporter des ennuis, les erreurs se glissant facilement, et obstruer l’évolutivité, la lisibilité, et la généricité.