Greg's Devblog Par un développeur, pour les développeurs

2oct/100

Non, il n’y a pas de comparaisons ternaires en C !

Un jour en TP à la fac un autre étudiant me demandait pourquoi je tapais le code suivant :

if ((0 < x) && (x < 2)){
    ...
}

Personnellement, ça ne me parait pas choquant... Mais lui me soutenait qu'en C on peut faire beaucoup plus "joli", avec le code suivant, qui parait effectivement plus naturel :

if (0 < x < 2){
    ...
}

Bon, je dirais, pourquoi pas... Après tout, ça compile, donc comme il semble le penser, si ça compile, c'est que ça marche ! Mais qu'en est-il en réalité ?
En pratique, l'opérateur de comparaison '<' (ainsi que tous ses acolytes) est un opérateur binaire, qui ne prend donc que 2 opérandes. Alors comment fonctionne cette forme avec 2 opérateurs et 3 opérandes ?

Petit quiz express : qui renvoie le code suivant ?

    printf("%i\n", (0 < 3 < 2));

Si vous avez répondu '0', vous avez perdu ! En effet, ça renvoie '1'. En fait, on pourrait changer le 3 par n'importe quelle valeur, on aurait toujours 1. Une fois qu'on a compris que cet opérateur est binaire, et qu'on applique donc le parenthésage par défaut défaut, on voit clairement à quoi correspond ce code :

(0 < 3) < 2

Si ce code compile, c'est uniquement parce qu'on peut effectivement l'évaluer "correctement". (0 < 3) donne un booléen, mais en C un booléen est juste un entier ayant pour valeurs 0 ou 1, donc on peut comparer ce booléen de la même manière avec 2. Or, (0 < 2) et (1 < 2) renvoient toujours "1", quoi qu'il arrive. Donc ce test est juste foireux.

Ceci fait ressortir à nouveau l'importance de mettre des parenthèses, mais aussi les limites du typage en C : comme tout est un peu du même type (tout est plus ou moins entier, sauf les pointeurs, mais si on veut on peut les caster de toute façon), on peut écrire des opérations qui sont parfaitement fausses. Le même code en Java ne compilerait a priori pas (je n'ai pas testé), puisque la comparaison entre un booléen et un entier n'est pas autorisée.

Une fois encore, il ne faut surtout pas partir du principe que ça fonctionne si on n'a pas vérifié que le langage le supporte ! Et une compilation réussie ne garanti en aucun cas que le code va faire ce qu'on pense qu'il va faire !

27sept/104

Qu’affiche le code suivant ?

Sous ce titre un peu racoleur se cache pourtant un phénomène assez étrange, qui fait qu'en C il n'est pas toujours facile de déterminer le résultat d'une instruction...

Voici donc un petit programme rapide, de quelques lignes ; je vous invite à essayer de deviner ce qu'il affiche...

#include <stdio.h>

int main(int argc, char **argv){
	int w = 0, x = 0, y = 0, z = 0;

	printf("w : %i %i\n", w++, w);
	printf("x : %i %i\n", x++, x+1);
	printf("y : %i %i\n", y, y++);
	printf("z : %i %i\n", z+1, z++);
	return 0;
}

Ca va paraître bizarre, mais en compilant le code sous OSX avec GCC je n'ai pas obtenu le même résultat que sous Windows (pourtant toujours avec GCC, mais probablement avec des options de compilation différentes). En utilisant cygwin et en compilant en ligne de commande (sous Windows, donc), j'obtiens bien le même résultat que sous OSX et Linux... Voici donc les 2 résultats obtenus :

Avec Code::Blocks

w : 0 0
x : 0 1
y : 1 0
z : 2 0

Ici, le résultat parait dans l'ensemble cohérent, si en tient compte d'un élément bizarre : on dirait que les opérations sont effectuées de droite à gauche : si dans la partie de droite on incrémente la variable, alors dans la partie de gauche la valeur sera (valeur+1), comme en témoignent les lignes 3 et 4.

Avec GCC sans option

w : 0 1
x : 0 1
y : 1 0
z : 2 0

Autre résultat, autre constant : cette fois-ci le résultat parait moins cohérent. En effet, les lignes 1 et 2, affichent le même résultat, alors que dans un cas on affiche "w", et dans l'autre "x+1"... L'explication semblerait être qu'on effectue en premier les opérations, de droite à gauche, puis on met les valeurs. Donc pour "w++, w", on incrémente d'abord w, puis on met les valeurs dans les arugments, alors que pour "x++, x+1", on calcule en premier x+1, puis on incremente avec x++, et les 2 valeurs stockées sont utilisées pour les arguments.

Que peut-on déduire de cet exemple foireux ?

  • Savoir exactement ce que fait un code n'est pas forcément simple.
  • Les warnings ne sont pas là pour rien... En l’occurrence, en activant les warnings on obtient ici "warning: operation on `w' may be undefined"... Pourtant, pas mal de monde semble considérer que "si ça compile, c'est que c'est bon", et on se retrouve avec des programmes bourrés de warnings (et donc potentiellement de bugs dans ce genre).

D'ailleurs, j'utilise souvent Code::Blocks, qui a une manie assez bizarre : si on compile avec "build and run" et qu'il n'y a pas d'erreur, et même s'il y a des centaines de warnings, Clode::Blocks vide le log de compilation et n'affiche rien... Il faut alors recompiler (sans exécuter) pour voir apparaître les warnings (pour peu qu'on ait fait une modification rapide dans un des fichiers pour "forcer" la recompilation...).

Méfiance !

Taggé comme: , , 4 Commentaires
14sept/100

Moteur de lumières en 2d

Je suis tombé ce matin sur un article fort sympathique, qui explique comment faire pour gérer des lumières en 2d : http://gregouar.developpez.com/tutoriels/jeux/moteur-lumieres-dynamiques-2d/

Le code est en C++, et utilise SFML. Et voici le résultat :

C'est pas mal du tout, et l'ensemble a l'air bien expliqué et simple à suivre. Je vous invite aussi à aller voir sur le tuto, car il a mis en ligne une petite vidéo (un gif, mais pas grave) que je trouve assez impressionnant et beaucoup plus parlant que cette image fixe...

Le principe utilisé est qu'une source lumineuse (dans toutes les directions, ou dans une direction précise), peut au final être décomposée en plusieurs triangles. Il "suffit" alors pour chaque triangle de calculer judicieusement les intersections avec les murs du décor pour avoir une lumière qui s'arrête au niveau des murs. Le seul souci est qu'il faudra faire attention aux performances, car il peut rapidement devenir très coûteux de calculer toutes les intersections ^^ Mais le reste, c'est de l'optimisation :)

Si jamais j'ai l'utilité dans un futur jeu (ou application), je ne manquerai pas d'essayer !

 

13sept/100

C++ et destructeur non virtual

c++

A un moment j'ai eu un patron qui demandait tout le temps à tout le monde s'il faisait de l'algo... Je trouvais ça bizarre comme critère principal de recrutement, et puis un jour en discutant avec lui j'ai compris : "ce qu'il y a de bien avec l'algo, c'est que tu peux prouver que ton code est juste, sans même avoir besoin de l'exécuter".

Ce qui est dommage avec lui, c'est qu'on a perdu énormément de temps à débugger son code ^^ (il ne semblait pas capable de le faire lui-même). Pour un jeu DS qu'on a eu à faire (et dont je tairais le nom afin de préserver le peu de crédibilité dont je pourrais disposer), il avait mis en place tout un système de widget, façon Java Swing, pour gérer les menus du jeu. Bon, c'est super, je ne suis pas forcément contre, mais le menu en question c'était un écran avec 2 boutons (PLAY et OPTIONS), l'écran des options c'était 2 boutons (SOUND ON/OFF et CREDITS), et l'écran de crédits avec les noms qui défilent. 2-3 mois pour faire ça, je trouve ça long ^^ Surtout qu'au final, je me suis rendu compte que si on faisait la séquence suivante : menu -> jeu -> menu -> jeu, ça plantait par manque de mémoire.

Là, on est sur DS... Donc pas de Valgrind pour analyser la situation. En regardant le code, je ne voyais pas trop d'où venait le souci, tous les objets alloués avaient l'air d'être détruits... Du coup je me suis dit que j'allais tracer toutes les allocations/libérations afin de trouver l'origine du problème. Heureusement pour nous, on n'utilisait pas malloc et free, mais des fonctions Mem_Alloc et Mem_Free persos, qui appelaient des routines d'allocation/libération de la DS, et qui étaient aussi utilisés en C++ par 'new' et compagnie... Je rajoute donc des affichages de debug à en spammer la console :

    A 0xXXXXXXXX (NN bytes) pour les allocations
    F 0xXXXXXXXX pour les libérations

Je me retrouve donc avec une console remplie de milliers d'allocations/libérations. Juste ingérable ^^ Je rajoute donc un premier filtre qui permet de n'afficher que les allocations supérieurs à une certaines taille (genre 1ko je crois), car de toute façon on avait des fuites de l'ordre de 500ko... Ca faisait encore pas mal de données à traiter, du coup j'ai fait une petite appli vite fait qui prend en ligne de commande un fichier, qui le dump ligne par ligne, enregistre les alloc/free, et recrache tout ce qui fuit (en gros le boulot de Valgrind...). Bien pratique tout ça :p Car avec ça j'avais la liste de toutes les allocations qui fuyaient, et comme sur DS on pouvait mettre des breakpoints, j'ai pu mettre des

if (adr == 0xXXXXXXXX){
    printf("STOP HERE\n");
}

avec un breakpoint sur le printf...

Et là, je me rends compte que tous les trucs qui fuient sont en fait les widgets... La question est donc : pourquoi ? Parce que dans le code, on fait bien des "delete" sur ces objets...

La réponse a été trouvée sur le net : si un destructeur n'est pas déclaré virtual, on ne passe pas par le destructeur de la classe fille ! C'est tout con, mais il faut le savoir, sinon niveau fuite ça peut aller très vite (comme là).

Problème réglé !! (modulo quelques petites fuites ailleurs mais plus classiques, oublis de libération de la mémoire...)

Moralité : d'une part on peut prouver qu'un algo est juste et avoir une implémentation fausse (car se sont 2 choses différentes), d'autre part, on peut prouver qu'un algo est juste tout en ayant une faille dans le raisonnement, et donc la preuve est en fait fausse... Je me dis que ce n'est pas pour rien que JUnit est utilisé par pas mal de monde...

Et aussi : utiliser les bons outils (Valgrind ou autre) pour résoudre les bons problèmes, c'est top ! Ici, en l'occurence, il aura été plus rapide de coder un petit tool sur mesure plutôt que de chercher vainement partout dans le code...

31août/100

Utiliser le mot-clé « super » en C++

A force de faire du Java, on prend certains réflexes... L'un d'entre eux est d'utiliser le mot-clé "super" pour accéder spécifiquement à une méthode de la super-classe. Du coup quand en codant en C++ j'ai tapé "super" et que j'ai eu droit à une erreur de compilation, ça m'a fait tout drôle...

D'un point de vue pratique, il existe une raison très simple et pour laquelle super ne serait pas disponible en C++ : l'héritage multiple... A quelle classe accèderait-on ainsi ?

En fait, en C++, la façon d'accéder à une méthode d'une superclasse est simple : on utilise explicitement le nom de cette superclasse.

void Foo::haha(void){
	Bar::haha();
}

Ca permet effectivement de résoudre le problème posé par l'héritage multiple, et ça a le mérite d'être très clair.

Ok, mais que se passerait-il si on voulait utiliser "super" au lieu du nom ? Après tout, dans certains cas ça peut avoir du sens, comme quand on passe dans des visiteurs/notifiers de façon récursive en appelant à chaque fois la super-classe. Et bien, bonne nouvelle, il existe une façon très simple d'accomplir cela :
class Derived : public Base
{
   private :
      typedef Base super; // note that it could be hidden in protected/private section, instead

      // Etc.
} ;

En fait c'est tout simple, on ne fait que définir le type super, pour cette classe, comme étant du type de la superclasse. Un premier avantage de cette méthode est que même avec l'héritage multiple, on peut faire fonctionner le mot-clé super en précisant soi-même quelle est la classe à "privilégier". Par contre, on ne peut en choisir qu'une :p

Cette méthode présente cependant un inconvénient majeur, qui peut être source de bug, et qui explique que super soit déclaré comme private (et non public ou protected) : que se passerait-il si on oubliait de le définir dans une sous-classe, et qu'on utilisait quand même le mot-clé super ? Et bien c'est simple, on n'irait pas du tout à la super-classe directe, mais à la super-classe précisée dans le typedef (donc plusieurs niveaux au-dessus, en gros). Dans le genre bug super galère à retrouver, ça peut être violent. Donc autant ne pas prendre le risque et bloquer ce type en le mettant private. Ca signifie qu'on ne pourra pas faire super::super::maMethode(), mais tant pis, il vaut parfois mieux jouer la sécurité.

Je dois dire que quelque part ça m'a fait plaisir de voir cette "bidouille" en C++, car pour moi le C et le C++ sont des langages où avec un peu d'astuce on peut un peu tout faire :) (sauf que le GC, il faut le recoder, mais bon...). En tout cas avec de bonnes petites macros et autres, on peut se simplifier la vie, là où dans d'autres langages (comme le Java) on va se reposer à la place sur l'IDE, ses facilités, et éventuellement en branchant par-dessus des systèmes de macros supplémentaires.

Source : Stack Overflow (probablement le meilleur site de questions/réponses pour les développeurs)

Taggé comme: , Aucun commentaire
9juil/100

C++0x : l’avenir du C++ ?

Hier je suis tombé un peu par hasard (ou pas d'ailleurs) sur les specs du C++0x (qui devrait maintenant s'appeler C++1x), norme qui devrait remplacer à terme le C++ tel qu'on le connait aujourd'hui...

J'étais assez sceptique car dernièrement je trouve que le C/C++ n'avance pas trop (par rapport à ce qui se fait partout ailleurs), mais je dois admettre que j'ai eu quelques bonnes surprises (et quelques mauvaises, dommage ^^). Voici quelques points que j'ai trouvé intéressants à signaler. 

9juil/100

Char – Signed or Unsigned ?

Enigme du jour: le jeu codé marche sur PC, sur iPhone, sur le simulateur Bada, mais plante lamentablement (et sans infos de debug) sur téléphone Bada. Pourquoi ?

Taggé comme: , , , , Lire la suite