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 !
Importance des parenthèses
J'ai déjà eu le cas d'un programmeur qui ne mettait quasiment jamais de parenthèses, genre "ça sert à rien". Un if peut rapidement avoir une tête du style :
if (a == 2 && b == 3 && c == 4 && ... && z == -2)
Je trouve ça relativement peu lisible, au final, et je rajoute toujours les parenthèses qui vont bien. Voici donc un petit code intéressant pour illustrer les soucis potentiels :
printf("%i %i %i\n", (-2)&0xFF, 254, (-2)&0xFF == 254);
Alors... A votre avis, qu'est-ce qui va être affiché ?
- (-2)&0xFF affiche 254
- 254 affiche... 254 (ok, c'était facile)
- (-2)&0xFF == 254 affiche... 0 !
Et oui, on pourrait s'attendre à ce que ça affiche 1 (puisque (-2)&0xFF vaut 254), mais en pratique ce n'est pas le cas. Alors pourquoi ?
Tout simplement parce que ce qu'on obtient comme résultat est en fait le résultat de (-2) & (0xFF == 254). Comme 0xFF vaut 255, (0xFF == 254) vaut 0, et donc (-2) & 0 vaut 0... Dommage !
En fait, il vaut mieux mettre des parenthèses inutiles que de ne pas en mettre du tout... Car même avec trop de parenthèses, on sait ce qu'on va obtenir, alors que si on les retire cela signifie qu'on se repose complètement sur la priorité décidée par le compilo (ou plutôt la norme). Pas forcémnet une bonne chose à faire si on ne maitrise pas parfaitement le sujet !
Pour rebondir un peu sur ce cas (qui parait un peu improbable à cause du -2 qui traine), voici un que j'ai construit et qui renvoie toujours TRUE :
1&0xFF == 255
C++ et destructeur non virtual

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...
JNI : trop d’objets tue l’objet
Un charmant petit plantage sur Android tout à l'heure, alors que le même code marchait très bien sur d'autres JVM (celle de Sun Oracle, ainsi que JamVM). Heureusement, le message d'erreur de Dalvik est très explicite, et m'indique que côté JNI je dépasse le nombre autorisé de local refs (qui est de seulement 512).
En fait dans ce bout de code je génère une table des matières en Java à partir d'une table des matières en C++. Le souci, c'est que celle-ci possède genre 300 entrées, et qu'en Java pour chaque entrée je dois créer 1 objet et 1 String, soit 2 objets au total. 300*2 = 600 > 512... Game Over !
La solution est simple, et les explications sur le pourquoi du comment très clairement exposées sur le site de Sun :
(*env)->DeleteLocalRef(env, jstr);
Une fois que l'objet créé a été refilé comme référence à un autre objet Java, on n'a plus vraiment besoin de garder la référence locale (qui in fine ne sert qu'à s'assurer que le GC ne va pas venir libérer des objets qu'on vient d'allouer côté C...), et donc on peut supprimer la LocalRef.
JVM : plusieurs implémentations = plusieurs problèmes
L'open source, c'est génial. Java, c'est fantastique, c'est la liberté, tout le monde peut faire ce qu'il veut avec. Mais si chacun est libre de faire sa JVM, ça veut aussi dire qu'il existe un risque accru de problèmes.
C'était déjà le cas avec le Java sur mobiles, à l'époque des "vieux" Nokia et compagnie : chaque téléphone avait son lot de bugs inclus dans la JVM, et les développeurs devaient faire avec. Du coup, au lieu d'avoir une seule version d'un code qui marche partout (puisque c'était le principe du Java...), il fallait à l'époque être capable de patcher les différentes versions pour les différents téléphones... Pratique !
Cette semaine on a eu ce genre de problèmes, mais en utilisant des trucs standards : la JVM JamVM (qu'on utilise pour sa performance et sa compacité en mémoire) et la version JDBC/JNI de SQLite : avec la JVM de Sun, ça marche parfaitement, tandis qu'avec JamVM, on a un segmentation fault. C'est le genre d'erreur un peu gênant quand on fait du Java, puisque ce n'est pas sensé se produire ^^ Mais c'est l'utilisation de JNI, et donc de code natif, qui introduit ce genre de soucis.
Heureusement (façon de parler), le bug ne se produisait pas sur toutes les requêtes, et on s'est rendu compte qu'il ne se produisait qu'en cas de requête qui aurait du renvoyer un String à null... En mettant les mains dans le code de la couche JNI, en remontant les appels successifs, on a rapidement cerné le soucis :
JNIEXPORT jstring JNICALL Java_org_sqlite_NativeDB_column_1text(JNIEnv *env, jobject this, jlong stmt, jint col)
{
return (*env)->NewStringUTF(env, (const char*)sqlite3_column_text(toref(stmt), col));
}
Ce petit bout de code fait le pont entre la partie purement C de sqlite (qui renvoie donc un (char *)) et la partie JNI (qui doit renvoyer un String) : on créé un String à partir du (char *) en utilisant les routines fournies par la JVM... Et c'est là que tout s'explique : la JVM de Sun autorise qu'on lui file un pointeur à NULL, et renvoie alors null comme String, alors que JamVM ne l'autorise pas.
On a été assez étonné de tomber sur un problème comme ça (à la fois du côté de JamVM qui ne gère pas ça comme la JVM de Sun, et du côté de sqlitejdbc, qui pourrait jouer la sécurité en faisant la vérification à la main). Le patch a été facile, il suffit de rajouter un petit if...
Moralité ? On ne peut pas toujours faire confiance aux libs et outils qu'on utilise, parfois il faut savoir s'en méfier et mettre le nez dedans
JamVM ARM bugs
J'avais parlé dans un précédent billet d'un souci qu'on avait, avec une exception levée dans une méthode toString, pour convertir un long en String... Et bien le problème est de retour, et en fait ça vient de la machine virtuelle qu'on utilise : JamVM.
Le soucis est relativement con, et clairement énoncé dans le bugtracker : "unary minus (-) in jamvm 1.5.4 fails for long on ARM". Or, comment marche la méthode toString ? Si on a un nombre positif, on converti, et si on a un nombre négatif, on affiche '-' au début, on le rend positif, et on le converti. Du coup, la méthode toString va lever une exception et planter lamentablement dès qu'on va convertir un long négatif, puisqu'on n'est pas en mesure de le rendre positif...
Super !
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 ?
Java : fuites mémoires ??
Java, c'est bien, on ne peut pas avoir de fuites mémoires, puisqu'on a un Garbage Collector (GC)... En êtes-vous sûrs ?
Je pose cette question toute conne parce qu'au boulot, dans l'appli Java, on plante au bout d'un moment par manque de mémoire ^^ Alors ça peut paraitre bizarre dit comme ça, mais j'ai commencé à me documenter un peu sur le pourquoi du comment, et je trouve ça assez intéressant.
Code au rabais ?
Un taiwanais, ça coute pas cher. Mais est-ce que c'est bon ?
Je ne vais pas faire de généralités, mais j'ai trouvé un truc pas mal dans le code tout à l'heure (copié/collé d'un truc "pro" fait à Taiwan...). En fait c'était un bout de code pour convertir un double en 2 ints (un pour la partie entière, un pour la partie décimale). Donc en gros, on obtient le code suivant :
int p, d; double val = XXX; p = (int)val; val = val - p; while(val < 1) val *= 10; d = val;
Bizarrement de temps en temps ça bloque le programme... (on notera que la récupération de la partie décimale est aussi super bien pensée... genre 1.01 renvoie 1, tout comme 1.1 et 1.0001 (entre autres...) ^^)
Le retour du Taiwanais…
Je viens de me rappeler d'un autre bout de code "mystique" aperçu dans les sources ^^ Cette fois-ci ce n'était pas gênant car c'était dans une partie du code qu'on n'utilise pas, mais bon...
En gros, vous avez une classe tableau (ou liste ou je sais pas ce que c'était, mais bon, en gros des données quoi ^^). Avec une belle fonction pour ajouter des choses dedans (à la suite). Jusque là, rien de spécial me direz-vous... Sauf qu'à la fin de la fonction d'ajout, on trouve ça :
void MyClass::add(...) { ... // Add to array size++; // Seems to slow down and bug when there's to much data if (size > 230) size = 230; }On n'a jamais compris pourquoi la limite est à 230, mais bon ^^